Sfoglia il codice sorgente

Update the Alfred workflow.

oldj 3 anni fa
parent
commit
1a89a9b7c0

BIN
alfred/E4D66445-FD72-47A2-9EE6-7232A2BADE29.png


+ 3 - 0
alfred/Readme.txt

@@ -0,0 +1,3 @@
+SwitchHosts! is an App for switching hosts quickly.
+
+Homepage: https://oldj.github.io/SwitchHosts/

+ 0 - 44
alfred/alfred.py

@@ -1,44 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import sys
-# the workflow package below can be downloaded from:
-# https://github.com/deanishe/alfred-workflow/releases
-from workflow import Workflow, ICON_WEB, web
-
-
-def get_subtitle(item):
-    content = item.get('content', '')
-    return content.partition('\n')[0].strip()
-
-
-def main(wf):
-    url = 'http://127.0.0.1:50761/api/list'
-    r = web.get(url)
-
-    # throw an error if request failed
-    # Workflow will catch this and show it to the user
-    r.raise_for_status()
-
-    # Parse the JSON returned by pinboard and extract the posts
-    result = r.json()
-    items = result['data']
-
-    # Loop through the returned posts and add an item for each to
-    # the list of results for Alfred
-    for item in items:
-        on = item.get('on', False)
-        wf.add_item(
-            title=item.get('title', 'untitled'),
-            subtitle=get_subtitle(item),
-            arg=item.get('id'),
-            valid=True,
-            icon='on.png' if on else 'off.png',
-        )
-
-    # Send the results to Alfred as XML
-    wf.send_feedback()
-
-
-if __name__ == '__main__':
-    my_wf = Workflow()
-    sys.exit(my_wf.run(main))

BIN
alfred/icon.png


+ 179 - 120
alfred/info.plist

@@ -1,124 +1,183 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
-    <dict>
-        <key>bundleid</key>
-        <string>switchhosts.oldj.net</string>
-        <key>category</key>
-        <string>Productivity</string>
-        <key>connections</key>
-        <dict>
-            <key>E4D66445-FD72-47A2-9EE6-7232A2BADE29</key>
-            <array>
-                <dict>
-                    <key>destinationuid</key>
-                    <string>78D17FD5-9628-4901-A01A-511528D5FC14</string>
-                    <key>modifiers</key>
-                    <integer>0</integer>
-                    <key>modifiersubtext</key>
-                    <string></string>
-                    <key>vitoclose</key>
-                    <false/>
-                </dict>
-            </array>
-        </dict>
-        <key>createdby</key>
-        <string>oldj</string>
-        <key>description</key>
-        <string>SwitchHosts quickly!</string>
-        <key>disabled</key>
-        <false/>
-        <key>name</key>
-        <string>SwitchHosts!</string>
-        <key>objects</key>
-        <array>
-            <dict>
-                <key>config</key>
-                <dict>
-                    <key>concurrently</key>
-                    <false/>
-                    <key>escaping</key>
-                    <integer>102</integer>
-                    <key>script</key>
-                    <string>curl 'http://127.0.0.1:50761/api/toggle?id={query}'</string>
-                    <key>scriptargtype</key>
-                    <integer>0</integer>
-                    <key>scriptfile</key>
-                    <string></string>
-                    <key>type</key>
-                    <integer>0</integer>
-                </dict>
-                <key>type</key>
-                <string>alfred.workflow.action.script</string>
-                <key>uid</key>
-                <string>78D17FD5-9628-4901-A01A-511528D5FC14</string>
-                <key>version</key>
-                <integer>2</integer>
-            </dict>
-            <dict>
-                <key>config</key>
-                <dict>
-                    <key>alfredfiltersresults</key>
-                    <false/>
-                    <key>argumenttype</key>
-                    <integer>2</integer>
-                    <key>escaping</key>
-                    <integer>68</integer>
-                    <key>keyword</key>
-                    <string>swh</string>
-                    <key>queuedelaycustom</key>
-                    <integer>3</integer>
-                    <key>queuedelayimmediatelyinitially</key>
-                    <true/>
-                    <key>queuedelaymode</key>
-                    <integer>0</integer>
-                    <key>queuemode</key>
-                    <integer>1</integer>
-                    <key>runningsubtext</key>
-                    <string>loading...</string>
-                    <key>script</key>
-                    <string>python alfred.py</string>
-                    <key>scriptargtype</key>
-                    <integer>1</integer>
-                    <key>scriptfile</key>
-                    <string></string>
-                    <key>subtext</key>
-                    <string>Switch hosts quickly!</string>
-                    <key>title</key>
-                    <string>Show hosts..</string>
-                    <key>type</key>
-                    <integer>0</integer>
-                    <key>withspace</key>
-                    <false/>
-                </dict>
-                <key>type</key>
-                <string>alfred.workflow.input.scriptfilter</string>
-                <key>uid</key>
-                <string>E4D66445-FD72-47A2-9EE6-7232A2BADE29</string>
-                <key>version</key>
-                <integer>2</integer>
-            </dict>
-        </array>
-        <key>readme</key>
-        <string></string>
-        <key>uidata</key>
-        <dict>
-            <key>78D17FD5-9628-4901-A01A-511528D5FC14</key>
-            <dict>
-                <key>xpos</key>
-                <integer>340</integer>
-                <key>ypos</key>
-                <integer>30</integer>
-            </dict>
-            <key>E4D66445-FD72-47A2-9EE6-7232A2BADE29</key>
-            <dict>
-                <key>xpos</key>
-                <integer>120</integer>
-                <key>ypos</key>
-                <integer>30</integer>
-            </dict>
-        </dict>
-        <key>webaddress</key>
-        <string>https://oldj.github.io/SwitchHosts/</string>
-    </dict>
+<dict>
+	<key>bundleid</key>
+	<string>switchhosts.oldj.net</string>
+	<key>category</key>
+	<string>Tools</string>
+	<key>connections</key>
+	<dict>
+		<key>E4D66445-FD72-47A2-9EE6-7232A2BADE29</key>
+		<array>
+			<dict>
+				<key>destinationuid</key>
+				<string>78D17FD5-9628-4901-A01A-511528D5FC14</string>
+				<key>modifiers</key>
+				<integer>0</integer>
+				<key>modifiersubtext</key>
+				<string></string>
+				<key>vitoclose</key>
+				<false/>
+			</dict>
+		</array>
+	</dict>
+	<key>createdby</key>
+	<string>oldj</string>
+	<key>description</key>
+	<string>Switch hosts quickly!</string>
+	<key>disabled</key>
+	<false/>
+	<key>name</key>
+	<string>SwitchHosts</string>
+	<key>objects</key>
+	<array>
+		<dict>
+			<key>config</key>
+			<dict>
+				<key>concurrently</key>
+				<false/>
+				<key>escaping</key>
+				<integer>102</integer>
+				<key>script</key>
+				<string>curl 'http://127.0.0.1:50761/api/toggle?id={query}'</string>
+				<key>scriptargtype</key>
+				<integer>0</integer>
+				<key>scriptfile</key>
+				<string></string>
+				<key>type</key>
+				<integer>0</integer>
+			</dict>
+			<key>type</key>
+			<string>alfred.workflow.action.script</string>
+			<key>uid</key>
+			<string>78D17FD5-9628-4901-A01A-511528D5FC14</string>
+			<key>version</key>
+			<integer>2</integer>
+		</dict>
+		<dict>
+			<key>config</key>
+			<dict>
+				<key>alfredfiltersresults</key>
+				<false/>
+				<key>alfredfiltersresultsmatchmode</key>
+				<integer>0</integer>
+				<key>argumenttreatemptyqueryasnil</key>
+				<false/>
+				<key>argumenttrimmode</key>
+				<integer>0</integer>
+				<key>argumenttype</key>
+				<integer>2</integer>
+				<key>escaping</key>
+				<integer>68</integer>
+				<key>keyword</key>
+				<string>swh</string>
+				<key>queuedelaycustom</key>
+				<integer>3</integer>
+				<key>queuedelayimmediatelyinitially</key>
+				<true/>
+				<key>queuedelaymode</key>
+				<integer>0</integer>
+				<key>queuemode</key>
+				<integer>1</integer>
+				<key>runningsubtext</key>
+				<string>loading...</string>
+				<key>script</key>
+				<string>function makeItems(items) {
+  return items.map(item =&gt; {
+    return {
+      uid: item.id,
+      title: item.title,
+      arg: item.id,
+      icon: {path: item.on ? 'on.png' : 'off.png'},
+      subtitle: (item.content || '').split('\n')[0],
+    }
+  })
+}
+
+function run(argv) {
+  const server = 'http://127.0.0.1:50761'
+  // console.log(argv)
+  const queryURL = $.NSURL.URLWithString(`${server}/api/list`)
+  const requestData = $.NSData.dataWithContentsOfURL(queryURL)
+  const requestString = $.NSString.alloc.initWithDataEncoding(requestData, $.NSUTF8StringEncoding).js
+
+  let result
+  try {
+    result = JSON.parse(requestString)
+  } catch (e) {
+    console.log(e)
+    return JSON.stringify({
+      items: [{
+        uid: '0',
+        title: `API Error: ${server}`,
+        subtitle: 'Make sure SwitchHosts is running and the HTTP API interface is enabled.',
+        valid: false,
+      }]
+    })
+  }
+
+  if (result.success) {
+    return JSON.stringify({
+      items: makeItems(result.data)
+    })
+  }
+
+  return JSON.stringify({
+    items: [{
+      uid: '0',
+      title: `Error: ${result.message || result.code || 'Unknown'}`,
+      valid: false,
+    }]
+  })
+}</string>
+				<key>scriptargtype</key>
+				<integer>1</integer>
+				<key>scriptfile</key>
+				<string></string>
+				<key>subtext</key>
+				<string>Switch hosts quickly!</string>
+				<key>title</key>
+				<string>Show hosts..</string>
+				<key>type</key>
+				<integer>7</integer>
+				<key>withspace</key>
+				<false/>
+			</dict>
+			<key>type</key>
+			<string>alfred.workflow.input.scriptfilter</string>
+			<key>uid</key>
+			<string>E4D66445-FD72-47A2-9EE6-7232A2BADE29</string>
+			<key>version</key>
+			<integer>3</integer>
+		</dict>
+	</array>
+	<key>readme</key>
+	<string>This workflow is for the SwitchHosts App.
+
+https://swh.app</string>
+	<key>uidata</key>
+	<dict>
+		<key>78D17FD5-9628-4901-A01A-511528D5FC14</key>
+		<dict>
+			<key>xpos</key>
+			<integer>340</integer>
+			<key>ypos</key>
+			<integer>30</integer>
+		</dict>
+		<key>E4D66445-FD72-47A2-9EE6-7232A2BADE29</key>
+		<dict>
+			<key>xpos</key>
+			<integer>120</integer>
+			<key>ypos</key>
+			<integer>30</integer>
+		</dict>
+	</dict>
+	<key>variablesdontexport</key>
+	<array/>
+	<key>version</key>
+	<string>1.3.0</string>
+	<key>webaddress</key>
+	<string>https://swh.app</string>
+</dict>
 </plist>

BIN
alfred/off.png


+ 0 - 108
alfred/workflow/__init__.py

@@ -1,108 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-#
-# Copyright (c) 2014 Dean Jackson <[email protected]>
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2014-02-15
-#
-
-"""A helper library for `Alfred <http://www.alfredapp.com/>`_ workflows."""
-
-import os
-
-# Workflow objects
-from .workflow import Workflow, manager
-from .workflow3 import Variables, Workflow3
-
-# Exceptions
-from .workflow import PasswordNotFound, KeychainError
-
-# Icons
-from .workflow import (
-    ICON_ACCOUNT,
-    ICON_BURN,
-    ICON_CLOCK,
-    ICON_COLOR,
-    ICON_COLOUR,
-    ICON_EJECT,
-    ICON_ERROR,
-    ICON_FAVORITE,
-    ICON_FAVOURITE,
-    ICON_GROUP,
-    ICON_HELP,
-    ICON_HOME,
-    ICON_INFO,
-    ICON_NETWORK,
-    ICON_NOTE,
-    ICON_SETTINGS,
-    ICON_SWIRL,
-    ICON_SWITCH,
-    ICON_SYNC,
-    ICON_TRASH,
-    ICON_USER,
-    ICON_WARNING,
-    ICON_WEB,
-)
-
-# Filter matching rules
-from .workflow import (
-    MATCH_ALL,
-    MATCH_ALLCHARS,
-    MATCH_ATOM,
-    MATCH_CAPITALS,
-    MATCH_INITIALS,
-    MATCH_INITIALS_CONTAIN,
-    MATCH_INITIALS_STARTSWITH,
-    MATCH_STARTSWITH,
-    MATCH_SUBSTRING,
-)
-
-
-__title__ = 'Alfred-Workflow'
-__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
-__author__ = 'Dean Jackson'
-__licence__ = 'MIT'
-__copyright__ = 'Copyright 2014-2019 Dean Jackson'
-
-__all__ = [
-    'Variables',
-    'Workflow',
-    'Workflow3',
-    'manager',
-    'PasswordNotFound',
-    'KeychainError',
-    'ICON_ACCOUNT',
-    'ICON_BURN',
-    'ICON_CLOCK',
-    'ICON_COLOR',
-    'ICON_COLOUR',
-    'ICON_EJECT',
-    'ICON_ERROR',
-    'ICON_FAVORITE',
-    'ICON_FAVOURITE',
-    'ICON_GROUP',
-    'ICON_HELP',
-    'ICON_HOME',
-    'ICON_INFO',
-    'ICON_NETWORK',
-    'ICON_NOTE',
-    'ICON_SETTINGS',
-    'ICON_SWIRL',
-    'ICON_SWITCH',
-    'ICON_SYNC',
-    'ICON_TRASH',
-    'ICON_USER',
-    'ICON_WARNING',
-    'ICON_WEB',
-    'MATCH_ALL',
-    'MATCH_ALLCHARS',
-    'MATCH_ATOM',
-    'MATCH_CAPITALS',
-    'MATCH_INITIALS',
-    'MATCH_INITIALS_CONTAIN',
-    'MATCH_INITIALS_STARTSWITH',
-    'MATCH_STARTSWITH',
-    'MATCH_SUBSTRING',
-]

+ 0 - 290
alfred/workflow/background.py

@@ -1,290 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-#
-# Copyright (c) 2014 [email protected]
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2014-04-06
-#
-
-"""This module provides an API to run commands in background processes.
-
-Combine with the :ref:`caching API <caching-data>` to work from cached data
-while you fetch fresh data in the background.
-
-See :ref:`the User Manual <background-processes>` for more information
-and examples.
-"""
-
-from __future__ import print_function, unicode_literals
-
-import signal
-import sys
-import os
-import subprocess
-import pickle
-
-from workflow import Workflow
-
-__all__ = ['is_running', 'run_in_background']
-
-_wf = None
-
-
-def wf():
-    global _wf
-    if _wf is None:
-        _wf = Workflow()
-    return _wf
-
-
-def _log():
-    return wf().logger
-
-
-def _arg_cache(name):
-    """Return path to pickle cache file for arguments.
-
-    :param name: name of task
-    :type name: ``unicode``
-    :returns: Path to cache file
-    :rtype: ``unicode`` filepath
-
-    """
-    return wf().cachefile(name + '.argcache')
-
-
-def _pid_file(name):
-    """Return path to PID file for ``name``.
-
-    :param name: name of task
-    :type name: ``unicode``
-    :returns: Path to PID file for task
-    :rtype: ``unicode`` filepath
-
-    """
-    return wf().cachefile(name + '.pid')
-
-
-def _process_exists(pid):
-    """Check if a process with PID ``pid`` exists.
-
-    :param pid: PID to check
-    :type pid: ``int``
-    :returns: ``True`` if process exists, else ``False``
-    :rtype: ``Boolean``
-
-    """
-    try:
-        os.kill(pid, 0)
-    except OSError:  # not running
-        return False
-    return True
-
-
-def _job_pid(name):
-    """Get PID of job or `None` if job does not exist.
-
-    Args:
-        name (str): Name of job.
-
-    Returns:
-        int: PID of job process (or `None` if job doesn't exist).
-    """
-    pidfile = _pid_file(name)
-    if not os.path.exists(pidfile):
-        return
-
-    with open(pidfile, 'rb') as fp:
-        pid = int(fp.read())
-
-        if _process_exists(pid):
-            return pid
-
-    os.unlink(pidfile)
-
-
-def is_running(name):
-    """Test whether task ``name`` is currently running.
-
-    :param name: name of task
-    :type name: unicode
-    :returns: ``True`` if task with name ``name`` is running, else ``False``
-    :rtype: bool
-
-    """
-    if _job_pid(name) is not None:
-        return True
-
-    return False
-
-
-def _background(pidfile, stdin='/dev/null', stdout='/dev/null',
-                stderr='/dev/null'):  # pragma: no cover
-    """Fork the current process into a background daemon.
-
-    :param pidfile: file to write PID of daemon process to.
-    :type pidfile: filepath
-    :param stdin: where to read input
-    :type stdin: filepath
-    :param stdout: where to write stdout output
-    :type stdout: filepath
-    :param stderr: where to write stderr output
-    :type stderr: filepath
-
-    """
-    def _fork_and_exit_parent(errmsg, wait=False, write=False):
-        try:
-            pid = os.fork()
-            if pid > 0:
-                if write:  # write PID of child process to `pidfile`
-                    tmp = pidfile + '.tmp'
-                    with open(tmp, 'wb') as fp:
-                        fp.write(str(pid))
-                    os.rename(tmp, pidfile)
-                if wait:  # wait for child process to exit
-                    os.waitpid(pid, 0)
-                os._exit(0)
-        except OSError as err:
-            _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror)
-            raise err
-
-    # Do first fork and wait for second fork to finish.
-    _fork_and_exit_parent('fork #1 failed', wait=True)
-
-    # Decouple from parent environment.
-    os.chdir(wf().workflowdir)
-    os.setsid()
-
-    # Do second fork and write PID to pidfile.
-    _fork_and_exit_parent('fork #2 failed', write=True)
-
-    # Now I am a daemon!
-    # Redirect standard file descriptors.
-    si = open(stdin, 'r', 0)
-    so = open(stdout, 'a+', 0)
-    se = open(stderr, 'a+', 0)
-    if hasattr(sys.stdin, 'fileno'):
-        os.dup2(si.fileno(), sys.stdin.fileno())
-    if hasattr(sys.stdout, 'fileno'):
-        os.dup2(so.fileno(), sys.stdout.fileno())
-    if hasattr(sys.stderr, 'fileno'):
-        os.dup2(se.fileno(), sys.stderr.fileno())
-
-
-def kill(name, sig=signal.SIGTERM):
-    """Send a signal to job ``name`` via :func:`os.kill`.
-
-    .. versionadded:: 1.29
-
-    Args:
-        name (str): Name of the job
-        sig (int, optional): Signal to send (default: SIGTERM)
-
-    Returns:
-        bool: `False` if job isn't running, `True` if signal was sent.
-    """
-    pid = _job_pid(name)
-    if pid is None:
-        return False
-
-    os.kill(pid, sig)
-    return True
-
-
-def run_in_background(name, args, **kwargs):
-    r"""Cache arguments then call this script again via :func:`subprocess.call`.
-
-    :param name: name of job
-    :type name: unicode
-    :param args: arguments passed as first argument to :func:`subprocess.call`
-    :param \**kwargs: keyword arguments to :func:`subprocess.call`
-    :returns: exit code of sub-process
-    :rtype: int
-
-    When you call this function, it caches its arguments and then calls
-    ``background.py`` in a subprocess. The Python subprocess will load the
-    cached arguments, fork into the background, and then run the command you
-    specified.
-
-    This function will return as soon as the ``background.py`` subprocess has
-    forked, returning the exit code of *that* process (i.e. not of the command
-    you're trying to run).
-
-    If that process fails, an error will be written to the log file.
-
-    If a process is already running under the same name, this function will
-    return immediately and will not run the specified command.
-
-    """
-    if is_running(name):
-        _log().info('[%s] job already running', name)
-        return
-
-    argcache = _arg_cache(name)
-
-    # Cache arguments
-    with open(argcache, 'wb') as fp:
-        pickle.dump({'args': args, 'kwargs': kwargs}, fp)
-        _log().debug('[%s] command cached: %s', name, argcache)
-
-    # Call this script
-    cmd = ['/usr/bin/python', __file__, name]
-    _log().debug('[%s] passing job to background runner: %r', name, cmd)
-    retcode = subprocess.call(cmd)
-
-    if retcode:  # pragma: no cover
-        _log().error('[%s] background runner failed with %d', name, retcode)
-    else:
-        _log().debug('[%s] background job started', name)
-
-    return retcode
-
-
-def main(wf):  # pragma: no cover
-    """Run command in a background process.
-
-    Load cached arguments, fork into background, then call
-    :meth:`subprocess.call` with cached arguments.
-
-    """
-    log = wf.logger
-    name = wf.args[0]
-    argcache = _arg_cache(name)
-    if not os.path.exists(argcache):
-        msg = '[{0}] command cache not found: {1}'.format(name, argcache)
-        log.critical(msg)
-        raise IOError(msg)
-
-    # Fork to background and run command
-    pidfile = _pid_file(name)
-    _background(pidfile)
-
-    # Load cached arguments
-    with open(argcache, 'rb') as fp:
-        data = pickle.load(fp)
-
-    # Cached arguments
-    args = data['args']
-    kwargs = data['kwargs']
-
-    # Delete argument cache file
-    os.unlink(argcache)
-
-    try:
-        # Run the command
-        log.debug('[%s] running command: %r', name, args)
-
-        retcode = subprocess.call(args, **kwargs)
-
-        if retcode:
-            log.error('[%s] command failed with status %d', name, retcode)
-    finally:
-        os.unlink(pidfile)
-
-    log.debug('[%s] job complete', name)
-
-
-if __name__ == '__main__':  # pragma: no cover
-    wf().run(main)

+ 0 - 348
alfred/workflow/notify.py

@@ -1,348 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-#
-# Copyright (c) 2015 [email protected]
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2015-11-26
-#
-
-# TODO: Exclude this module from test and code coverage in py2.6
-
-"""
-Post notifications via the macOS Notification Center.
-
-This feature is only available on Mountain Lion (10.8) and later.
-It will silently fail on older systems.
-
-The main API is a single function, :func:`~workflow.notify.notify`.
-
-It works by copying a simple application to your workflow's data
-directory. It replaces the application's icon with your workflow's
-icon and then calls the application to post notifications.
-"""
-
-from __future__ import print_function, unicode_literals
-
-import os
-import plistlib
-import shutil
-import subprocess
-import sys
-import tarfile
-import tempfile
-import uuid
-
-import workflow
-
-
-_wf = None
-_log = None
-
-
-#: Available system sounds from System Preferences > Sound > Sound Effects
-SOUNDS = (
-    'Basso',
-    'Blow',
-    'Bottle',
-    'Frog',
-    'Funk',
-    'Glass',
-    'Hero',
-    'Morse',
-    'Ping',
-    'Pop',
-    'Purr',
-    'Sosumi',
-    'Submarine',
-    'Tink',
-)
-
-
-def wf():
-    """Return Workflow object for this module.
-
-    Returns:
-        workflow.Workflow: Workflow object for current workflow.
-    """
-    global _wf
-    if _wf is None:
-        _wf = workflow.Workflow()
-    return _wf
-
-
-def log():
-    """Return logger for this module.
-
-    Returns:
-        logging.Logger: Logger for this module.
-    """
-    global _log
-    if _log is None:
-        _log = wf().logger
-    return _log
-
-
-def notifier_program():
-    """Return path to notifier applet executable.
-
-    Returns:
-        unicode: Path to Notify.app ``applet`` executable.
-    """
-    return wf().datafile('Notify.app/Contents/MacOS/applet')
-
-
-def notifier_icon_path():
-    """Return path to icon file in installed Notify.app.
-
-    Returns:
-        unicode: Path to ``applet.icns`` within the app bundle.
-    """
-    return wf().datafile('Notify.app/Contents/Resources/applet.icns')
-
-
-def install_notifier():
-    """Extract ``Notify.app`` from the workflow to data directory.
-
-    Changes the bundle ID of the installed app and gives it the
-    workflow's icon.
-    """
-    archive = os.path.join(os.path.dirname(__file__), 'Notify.tgz')
-    destdir = wf().datadir
-    app_path = os.path.join(destdir, 'Notify.app')
-    n = notifier_program()
-    log().debug('installing Notify.app to %r ...', destdir)
-    # z = zipfile.ZipFile(archive, 'r')
-    # z.extractall(destdir)
-    tgz = tarfile.open(archive, 'r:gz')
-    tgz.extractall(destdir)
-    if not os.path.exists(n):  # pragma: nocover
-        raise RuntimeError('Notify.app could not be installed in ' + destdir)
-
-    # Replace applet icon
-    icon = notifier_icon_path()
-    workflow_icon = wf().workflowfile('icon.png')
-    if os.path.exists(icon):
-        os.unlink(icon)
-
-    png_to_icns(workflow_icon, icon)
-
-    # Set file icon
-    # PyObjC isn't available for 2.6, so this is 2.7 only. Actually,
-    # none of this code will "work" on pre-10.8 systems. Let it run
-    # until I figure out a better way of excluding this module
-    # from coverage in py2.6.
-    if sys.version_info >= (2, 7):  # pragma: no cover
-        from AppKit import NSWorkspace, NSImage
-
-        ws = NSWorkspace.sharedWorkspace()
-        img = NSImage.alloc().init()
-        img.initWithContentsOfFile_(icon)
-        ws.setIcon_forFile_options_(img, app_path, 0)
-
-    # Change bundle ID of installed app
-    ip_path = os.path.join(app_path, 'Contents/Info.plist')
-    bundle_id = '{0}.{1}'.format(wf().bundleid, uuid.uuid4().hex)
-    data = plistlib.readPlist(ip_path)
-    log().debug('changing bundle ID to %r', bundle_id)
-    data['CFBundleIdentifier'] = bundle_id
-    plistlib.writePlist(data, ip_path)
-
-
-def validate_sound(sound):
-    """Coerce ``sound`` to valid sound name.
-
-    Returns ``None`` for invalid sounds. Sound names can be found
-    in ``System Preferences > Sound > Sound Effects``.
-
-    Args:
-        sound (str): Name of system sound.
-
-    Returns:
-        str: Proper name of sound or ``None``.
-    """
-    if not sound:
-        return None
-
-    # Case-insensitive comparison of `sound`
-    if sound.lower() in [s.lower() for s in SOUNDS]:
-        # Title-case is correct for all system sounds as of macOS 10.11
-        return sound.title()
-    return None
-
-
-def notify(title='', text='', sound=None):
-    """Post notification via Notify.app helper.
-
-    Args:
-        title (str, optional): Notification title.
-        text (str, optional): Notification body text.
-        sound (str, optional): Name of sound to play.
-
-    Raises:
-        ValueError: Raised if both ``title`` and ``text`` are empty.
-
-    Returns:
-        bool: ``True`` if notification was posted, else ``False``.
-    """
-    if title == text == '':
-        raise ValueError('Empty notification')
-
-    sound = validate_sound(sound) or ''
-
-    n = notifier_program()
-
-    if not os.path.exists(n):
-        install_notifier()
-
-    env = os.environ.copy()
-    enc = 'utf-8'
-    env['NOTIFY_TITLE'] = title.encode(enc)
-    env['NOTIFY_MESSAGE'] = text.encode(enc)
-    env['NOTIFY_SOUND'] = sound.encode(enc)
-    cmd = [n]
-    retcode = subprocess.call(cmd, env=env)
-    if retcode == 0:
-        return True
-
-    log().error('Notify.app exited with status {0}.'.format(retcode))
-    return False
-
-
-def convert_image(inpath, outpath, size):
-    """Convert an image file using ``sips``.
-
-    Args:
-        inpath (str): Path of source file.
-        outpath (str): Path to destination file.
-        size (int): Width and height of destination image in pixels.
-
-    Raises:
-        RuntimeError: Raised if ``sips`` exits with non-zero status.
-    """
-    cmd = [
-        b'sips',
-        b'-z', str(size), str(size),
-        inpath,
-        b'--out', outpath]
-    # log().debug(cmd)
-    with open(os.devnull, 'w') as pipe:
-        retcode = subprocess.call(cmd, stdout=pipe, stderr=subprocess.STDOUT)
-
-    if retcode != 0:
-        raise RuntimeError('sips exited with %d' % retcode)
-
-
-def png_to_icns(png_path, icns_path):
-    """Convert PNG file to ICNS using ``iconutil``.
-
-    Create an iconset from the source PNG file. Generate PNG files
-    in each size required by macOS, then call ``iconutil`` to turn
-    them into a single ICNS file.
-
-    Args:
-        png_path (str): Path to source PNG file.
-        icns_path (str): Path to destination ICNS file.
-
-    Raises:
-        RuntimeError: Raised if ``iconutil`` or ``sips`` fail.
-    """
-    tempdir = tempfile.mkdtemp(prefix='aw-', dir=wf().datadir)
-
-    try:
-        iconset = os.path.join(tempdir, 'Icon.iconset')
-
-        if os.path.exists(iconset):  # pragma: nocover
-            raise RuntimeError('iconset already exists: ' + iconset)
-
-        os.makedirs(iconset)
-
-        # Copy source icon to icon set and generate all the other
-        # sizes needed
-        configs = []
-        for i in (16, 32, 128, 256, 512):
-            configs.append(('icon_{0}x{0}.png'.format(i), i))
-            configs.append((('icon_{0}x{0}@2x.png'.format(i), i * 2)))
-
-        shutil.copy(png_path, os.path.join(iconset, 'icon_256x256.png'))
-        shutil.copy(png_path, os.path.join(iconset, '[email protected]'))
-
-        for name, size in configs:
-            outpath = os.path.join(iconset, name)
-            if os.path.exists(outpath):
-                continue
-            convert_image(png_path, outpath, size)
-
-        cmd = [
-            b'iconutil',
-            b'-c', b'icns',
-            b'-o', icns_path,
-            iconset]
-
-        retcode = subprocess.call(cmd)
-        if retcode != 0:
-            raise RuntimeError('iconset exited with %d' % retcode)
-
-        if not os.path.exists(icns_path):  # pragma: nocover
-            raise ValueError(
-                'generated ICNS file not found: ' + repr(icns_path))
-    finally:
-        try:
-            shutil.rmtree(tempdir)
-        except OSError:  # pragma: no cover
-            pass
-
-
-if __name__ == '__main__':  # pragma: nocover
-    # Simple command-line script to test module with
-    # This won't work on 2.6, as `argparse` isn't available
-    # by default.
-    import argparse
-
-    from unicodedata import normalize
-
-    def ustr(s):
-        """Coerce `s` to normalised Unicode."""
-        return normalize('NFD', s.decode('utf-8'))
-
-    p = argparse.ArgumentParser()
-    p.add_argument('-p', '--png', help="PNG image to convert to ICNS.")
-    p.add_argument('-l', '--list-sounds', help="Show available sounds.",
-                   action='store_true')
-    p.add_argument('-t', '--title',
-                   help="Notification title.", type=ustr,
-                   default='')
-    p.add_argument('-s', '--sound', type=ustr,
-                   help="Optional notification sound.", default='')
-    p.add_argument('text', type=ustr,
-                   help="Notification body text.", default='', nargs='?')
-    o = p.parse_args()
-
-    # List available sounds
-    if o.list_sounds:
-        for sound in SOUNDS:
-            print(sound)
-        sys.exit(0)
-
-    # Convert PNG to ICNS
-    if o.png:
-        icns = os.path.join(
-            os.path.dirname(o.png),
-            os.path.splitext(os.path.basename(o.png))[0] + '.icns')
-
-        print('converting {0!r} to {1!r} ...'.format(o.png, icns),
-              file=sys.stderr)
-
-        if os.path.exists(icns):
-            raise ValueError('destination file already exists: ' + icns)
-
-        png_to_icns(o.png, icns)
-        sys.exit(0)
-
-    # Post notification
-    if o.title == o.text == '':
-        print('ERROR: empty notification.', file=sys.stderr)
-        sys.exit(1)
-    else:
-        notify(o.title, o.text, o.sound)

+ 0 - 565
alfred/workflow/update.py

@@ -1,565 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-#
-# Copyright (c) 2014 Fabio Niephaus <[email protected]>,
-#       Dean Jackson <[email protected]>
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2014-08-16
-#
-
-"""Self-updating from GitHub.
-
-.. versionadded:: 1.9
-
-.. note::
-
-   This module is not intended to be used directly. Automatic updates
-   are controlled by the ``update_settings`` :class:`dict` passed to
-   :class:`~workflow.workflow.Workflow` objects.
-
-"""
-
-from __future__ import print_function, unicode_literals
-
-from collections import defaultdict
-from functools import total_ordering
-import json
-import os
-import tempfile
-import re
-import subprocess
-
-import workflow
-import web
-
-# __all__ = []
-
-
-RELEASES_BASE = 'https://api.github.com/repos/{}/releases'
-match_workflow = re.compile(r'\.alfred(\d+)?workflow$').search
-
-_wf = None
-
-
-def wf():
-    """Lazy `Workflow` object."""
-    global _wf
-    if _wf is None:
-        _wf = workflow.Workflow()
-    return _wf
-
-
-@total_ordering
-class Download(object):
-    """A workflow file that is available for download.
-
-    .. versionadded: 1.37
-
-    Attributes:
-        url (str): URL of workflow file.
-        filename (str): Filename of workflow file.
-        version (Version): Semantic version of workflow.
-        prerelease (bool): Whether version is a pre-release.
-        alfred_version (Version): Minimum compatible version
-            of Alfred.
-
-    """
-
-    @classmethod
-    def from_dict(cls, d):
-        """Create a `Download` from a `dict`."""
-        return cls(url=d['url'], filename=d['filename'],
-                   version=Version(d['version']),
-                   prerelease=d['prerelease'])
-
-    @classmethod
-    def from_releases(cls, js):
-        """Extract downloads from GitHub releases.
-
-        Searches releases with semantic tags for assets with
-        file extension .alfredworkflow or .alfredXworkflow where
-        X is a number.
-
-        Files are returned sorted by latest version first. Any
-        releases containing multiple files with the same (workflow)
-        extension are rejected as ambiguous.
-
-        Args:
-            js (str): JSON response from GitHub's releases endpoint.
-
-        Returns:
-            list: Sequence of `Download`.
-        """
-        releases = json.loads(js)
-        downloads = []
-        for release in releases:
-            tag = release['tag_name']
-            dupes = defaultdict(int)
-            try:
-                version = Version(tag)
-            except ValueError as err:
-                wf().logger.debug('ignored release: bad version "%s": %s',
-                                  tag, err)
-                continue
-
-            dls = []
-            for asset in release.get('assets', []):
-                url = asset.get('browser_download_url')
-                filename = os.path.basename(url)
-                m = match_workflow(filename)
-                if not m:
-                    wf().logger.debug('unwanted file: %s', filename)
-                    continue
-
-                ext = m.group(0)
-                dupes[ext] = dupes[ext] + 1
-                dls.append(Download(url, filename, version,
-                                    release['prerelease']))
-
-            valid = True
-            for ext, n in dupes.items():
-                if n > 1:
-                    wf().logger.debug('ignored release "%s": multiple assets '
-                                      'with extension "%s"', tag, ext)
-                    valid = False
-                    break
-
-            if valid:
-                downloads.extend(dls)
-
-        downloads.sort(reverse=True)
-        return downloads
-
-    def __init__(self, url, filename, version, prerelease=False):
-        """Create a new Download.
-
-        Args:
-            url (str): URL of workflow file.
-            filename (str): Filename of workflow file.
-            version (Version): Version of workflow.
-            prerelease (bool, optional): Whether version is
-                pre-release. Defaults to False.
-
-        """
-        if isinstance(version, basestring):
-            version = Version(version)
-
-        self.url = url
-        self.filename = filename
-        self.version = version
-        self.prerelease = prerelease
-
-    @property
-    def alfred_version(self):
-        """Minimum Alfred version based on filename extension."""
-        m = match_workflow(self.filename)
-        if not m or not m.group(1):
-            return Version('0')
-        return Version(m.group(1))
-
-    @property
-    def dict(self):
-        """Convert `Download` to `dict`."""
-        return dict(url=self.url, filename=self.filename,
-                    version=str(self.version), prerelease=self.prerelease)
-
-    def __str__(self):
-        """Format `Download` for printing."""
-        u = ('Download(url={dl.url!r}, '
-             'filename={dl.filename!r}, '
-             'version={dl.version!r}, '
-             'prerelease={dl.prerelease!r})'.format(dl=self))
-
-        return u.encode('utf-8')
-
-    def __repr__(self):
-        """Code-like representation of `Download`."""
-        return str(self)
-
-    def __eq__(self, other):
-        """Compare Downloads based on version numbers."""
-        if self.url != other.url \
-                or self.filename != other.filename \
-                or self.version != other.version \
-                or self.prerelease != other.prerelease:
-            return False
-        return True
-
-    def __ne__(self, other):
-        """Compare Downloads based on version numbers."""
-        return not self.__eq__(other)
-
-    def __lt__(self, other):
-        """Compare Downloads based on version numbers."""
-        if self.version != other.version:
-            return self.version < other.version
-        return self.alfred_version < other.alfred_version
-
-
-class Version(object):
-    """Mostly semantic versioning.
-
-    The main difference to proper :ref:`semantic versioning <semver>`
-    is that this implementation doesn't require a minor or patch version.
-
-    Version strings may also be prefixed with "v", e.g.:
-
-    >>> v = Version('v1.1.1')
-    >>> v.tuple
-    (1, 1, 1, '')
-
-    >>> v = Version('2.0')
-    >>> v.tuple
-    (2, 0, 0, '')
-
-    >>> Version('3.1-beta').tuple
-    (3, 1, 0, 'beta')
-
-    >>> Version('1.0.1') > Version('0.0.1')
-    True
-    """
-
-    #: Match version and pre-release/build information in version strings
-    match_version = re.compile(r'([0-9][0-9\.]*)(.+)?').match
-
-    def __init__(self, vstr):
-        """Create new `Version` object.
-
-        Args:
-            vstr (basestring): Semantic version string.
-        """
-        if not vstr:
-            raise ValueError('invalid version number: {!r}'.format(vstr))
-
-        self.vstr = vstr
-        self.major = 0
-        self.minor = 0
-        self.patch = 0
-        self.suffix = ''
-        self.build = ''
-        self._parse(vstr)
-
-    def _parse(self, vstr):
-        if vstr.startswith('v'):
-            m = self.match_version(vstr[1:])
-        else:
-            m = self.match_version(vstr)
-        if not m:
-            raise ValueError('invalid version number: ' + vstr)
-
-        version, suffix = m.groups()
-        parts = self._parse_dotted_string(version)
-        self.major = parts.pop(0)
-        if len(parts):
-            self.minor = parts.pop(0)
-        if len(parts):
-            self.patch = parts.pop(0)
-        if not len(parts) == 0:
-            raise ValueError('version number too long: ' + vstr)
-
-        if suffix:
-            # Build info
-            idx = suffix.find('+')
-            if idx > -1:
-                self.build = suffix[idx+1:]
-                suffix = suffix[:idx]
-            if suffix:
-                if not suffix.startswith('-'):
-                    raise ValueError(
-                        'suffix must start with - : ' + suffix)
-                self.suffix = suffix[1:]
-
-    def _parse_dotted_string(self, s):
-        """Parse string ``s`` into list of ints and strings."""
-        parsed = []
-        parts = s.split('.')
-        for p in parts:
-            if p.isdigit():
-                p = int(p)
-            parsed.append(p)
-        return parsed
-
-    @property
-    def tuple(self):
-        """Version number as a tuple of major, minor, patch, pre-release."""
-        return (self.major, self.minor, self.patch, self.suffix)
-
-    def __lt__(self, other):
-        """Implement comparison."""
-        if not isinstance(other, Version):
-            raise ValueError('not a Version instance: {0!r}'.format(other))
-        t = self.tuple[:3]
-        o = other.tuple[:3]
-        if t < o:
-            return True
-        if t == o:  # We need to compare suffixes
-            if self.suffix and not other.suffix:
-                return True
-            if other.suffix and not self.suffix:
-                return False
-            return self._parse_dotted_string(self.suffix) \
-                < self._parse_dotted_string(other.suffix)
-        # t > o
-        return False
-
-    def __eq__(self, other):
-        """Implement comparison."""
-        if not isinstance(other, Version):
-            raise ValueError('not a Version instance: {0!r}'.format(other))
-        return self.tuple == other.tuple
-
-    def __ne__(self, other):
-        """Implement comparison."""
-        return not self.__eq__(other)
-
-    def __gt__(self, other):
-        """Implement comparison."""
-        if not isinstance(other, Version):
-            raise ValueError('not a Version instance: {0!r}'.format(other))
-        return other.__lt__(self)
-
-    def __le__(self, other):
-        """Implement comparison."""
-        if not isinstance(other, Version):
-            raise ValueError('not a Version instance: {0!r}'.format(other))
-        return not other.__lt__(self)
-
-    def __ge__(self, other):
-        """Implement comparison."""
-        return not self.__lt__(other)
-
-    def __str__(self):
-        """Return semantic version string."""
-        vstr = '{0}.{1}.{2}'.format(self.major, self.minor, self.patch)
-        if self.suffix:
-            vstr = '{0}-{1}'.format(vstr, self.suffix)
-        if self.build:
-            vstr = '{0}+{1}'.format(vstr, self.build)
-        return vstr
-
-    def __repr__(self):
-        """Return 'code' representation of `Version`."""
-        return "Version('{0}')".format(str(self))
-
-
-def retrieve_download(dl):
-    """Saves a download to a temporary file and returns path.
-
-    .. versionadded: 1.37
-
-    Args:
-        url (unicode): URL to .alfredworkflow file in GitHub repo
-
-    Returns:
-        unicode: path to downloaded file
-
-    """
-    if not match_workflow(dl.filename):
-        raise ValueError('attachment not a workflow: ' + dl.filename)
-
-    path = os.path.join(tempfile.gettempdir(), dl.filename)
-    wf().logger.debug('downloading update from '
-                      '%r to %r ...', dl.url, path)
-
-    r = web.get(dl.url)
-    r.raise_for_status()
-
-    r.save_to_path(path)
-
-    return path
-
-
-def build_api_url(repo):
-    """Generate releases URL from GitHub repo.
-
-    Args:
-        repo (unicode): Repo name in form ``username/repo``
-
-    Returns:
-        unicode: URL to the API endpoint for the repo's releases
-
-    """
-    if len(repo.split('/')) != 2:
-        raise ValueError('invalid GitHub repo: {!r}'.format(repo))
-
-    return RELEASES_BASE.format(repo)
-
-
-def get_downloads(repo):
-    """Load available ``Download``s for GitHub repo.
-
-    .. versionadded: 1.37
-
-    Args:
-        repo (unicode): GitHub repo to load releases for.
-
-    Returns:
-        list: Sequence of `Download` contained in GitHub releases.
-    """
-    url = build_api_url(repo)
-
-    def _fetch():
-        wf().logger.info('retrieving releases for %r ...', repo)
-        r = web.get(url)
-        r.raise_for_status()
-        return r.content
-
-    key = 'github-releases-' + repo.replace('/', '-')
-    js = wf().cached_data(key, _fetch, max_age=60)
-
-    return Download.from_releases(js)
-
-
-def latest_download(dls, alfred_version=None, prereleases=False):
-    """Return newest `Download`."""
-    alfred_version = alfred_version or os.getenv('alfred_version')
-    version = None
-    if alfred_version:
-        version = Version(alfred_version)
-
-    dls.sort(reverse=True)
-    for dl in dls:
-        if dl.prerelease and not prereleases:
-            wf().logger.debug('ignored prerelease: %s', dl.version)
-            continue
-        if version and dl.alfred_version > version:
-            wf().logger.debug('ignored incompatible (%s > %s): %s',
-                              dl.alfred_version, version, dl.filename)
-            continue
-
-        wf().logger.debug('latest version: %s (%s)', dl.version, dl.filename)
-        return dl
-
-    return None
-
-
-def check_update(repo, current_version, prereleases=False,
-                 alfred_version=None):
-    """Check whether a newer release is available on GitHub.
-
-    Args:
-        repo (unicode): ``username/repo`` for workflow's GitHub repo
-        current_version (unicode): the currently installed version of the
-            workflow. :ref:`Semantic versioning <semver>` is required.
-        prereleases (bool): Whether to include pre-releases.
-        alfred_version (unicode): version of currently-running Alfred.
-            if empty, defaults to ``$alfred_version`` environment variable.
-
-    Returns:
-        bool: ``True`` if an update is available, else ``False``
-
-    If an update is available, its version number and download URL will
-    be cached.
-
-    """
-    key = '__workflow_latest_version'
-    # data stored when no update is available
-    no_update = {
-        'available': False,
-        'download': None,
-        'version': None,
-    }
-    current = Version(current_version)
-
-    dls = get_downloads(repo)
-    if not len(dls):
-        wf().logger.warning('no valid downloads for %s', repo)
-        wf().cache_data(key, no_update)
-        return False
-
-    wf().logger.info('%d download(s) for %s', len(dls), repo)
-
-    dl = latest_download(dls, alfred_version, prereleases)
-
-    if not dl:
-        wf().logger.warning('no compatible downloads for %s', repo)
-        wf().cache_data(key, no_update)
-        return False
-
-    wf().logger.debug('latest=%r, installed=%r', dl.version, current)
-
-    if dl.version > current:
-        wf().cache_data(key, {
-            'version': str(dl.version),
-            'download': dl.dict,
-            'available': True,
-        })
-        return True
-
-    wf().cache_data(key, no_update)
-    return False
-
-
-def install_update():
-    """If a newer release is available, download and install it.
-
-    :returns: ``True`` if an update is installed, else ``False``
-
-    """
-    key = '__workflow_latest_version'
-    # data stored when no update is available
-    no_update = {
-        'available': False,
-        'download': None,
-        'version': None,
-    }
-    status = wf().cached_data(key, max_age=0)
-
-    if not status or not status.get('available'):
-        wf().logger.info('no update available')
-        return False
-
-    dl = status.get('download')
-    if not dl:
-        wf().logger.info('no download information')
-        return False
-
-    path = retrieve_download(Download.from_dict(dl))
-
-    wf().logger.info('installing updated workflow ...')
-    subprocess.call(['open', path])  # nosec
-
-    wf().cache_data(key, no_update)
-    return True
-
-
-if __name__ == '__main__':  # pragma: nocover
-    import sys
-
-    prereleases = False
-
-    def show_help(status=0):
-        """Print help message."""
-        print('usage: update.py (check|install) '
-              '[--prereleases] <repo> <version>')
-        sys.exit(status)
-
-    argv = sys.argv[:]
-    if '-h' in argv or '--help' in argv:
-        show_help()
-
-    if '--prereleases' in argv:
-        argv.remove('--prereleases')
-        prereleases = True
-
-    if len(argv) != 4:
-        show_help(1)
-
-    action = argv[1]
-    repo = argv[2]
-    version = argv[3]
-
-    try:
-
-        if action == 'check':
-            check_update(repo, version, prereleases)
-        elif action == 'install':
-            install_update()
-        else:
-            show_help(1)
-
-    except Exception as err:  # ensure traceback is in log file
-        wf().logger.exception(err)
-        raise err

+ 0 - 644
alfred/workflow/util.py

@@ -1,644 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-#
-# Copyright (c) 2017 Dean Jackson <[email protected]>
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2017-12-17
-#
-
-"""A selection of helper functions useful for building workflows."""
-
-from __future__ import print_function, absolute_import
-
-import atexit
-from collections import namedtuple
-from contextlib import contextmanager
-import errno
-import fcntl
-import functools
-import json
-import os
-import signal
-import subprocess
-import sys
-from threading import Event
-import time
-
-# JXA scripts to call Alfred's API via the Scripting Bridge
-# {app} is automatically replaced with "Alfred 3" or
-# "com.runningwithcrayons.Alfred" depending on version.
-#
-# Open Alfred in search (regular) mode
-JXA_SEARCH = 'Application({app}).search({arg});'
-# Open Alfred's File Actions on an argument
-JXA_ACTION = 'Application({app}).action({arg});'
-# Open Alfred's navigation mode at path
-JXA_BROWSE = 'Application({app}).browse({arg});'
-# Set the specified theme
-JXA_SET_THEME = 'Application({app}).setTheme({arg});'
-# Call an External Trigger
-JXA_TRIGGER = 'Application({app}).runTrigger({arg}, {opts});'
-# Save a variable to the workflow configuration sheet/info.plist
-JXA_SET_CONFIG = 'Application({app}).setConfiguration({arg}, {opts});'
-# Delete a variable from the workflow configuration sheet/info.plist
-JXA_UNSET_CONFIG = 'Application({app}).removeConfiguration({arg}, {opts});'
-# Tell Alfred to reload a workflow from disk
-JXA_RELOAD_WORKFLOW = 'Application({app}).reloadWorkflow({arg});'
-
-
-class AcquisitionError(Exception):
-    """Raised if a lock cannot be acquired."""
-
-
-AppInfo = namedtuple('AppInfo', ['name', 'path', 'bundleid'])
-"""Information about an installed application.
-
-Returned by :func:`appinfo`. All attributes are Unicode.
-
-.. py:attribute:: name
-
-    Name of the application, e.g. ``u'Safari'``.
-
-.. py:attribute:: path
-
-    Path to the application bundle, e.g. ``u'/Applications/Safari.app'``.
-
-.. py:attribute:: bundleid
-
-    Application's bundle ID, e.g. ``u'com.apple.Safari'``.
-
-"""
-
-
-def jxa_app_name():
-    """Return name of application to call currently running Alfred.
-
-    .. versionadded: 1.37
-
-    Returns 'Alfred 3' or 'com.runningwithcrayons.Alfred' depending
-    on which version of Alfred is running.
-
-    This name is suitable for use with ``Application(name)`` in JXA.
-
-    Returns:
-        unicode: Application name or ID.
-
-    """
-    if os.getenv('alfred_version', '').startswith('3'):
-        # Alfred 3
-        return u'Alfred 3'
-    # Alfred 4+
-    return u'com.runningwithcrayons.Alfred'
-
-
-def unicodify(s, encoding='utf-8', norm=None):
-    """Ensure string is Unicode.
-
-    .. versionadded:: 1.31
-
-    Decode encoded strings using ``encoding`` and normalise Unicode
-    to form ``norm`` if specified.
-
-    Args:
-        s (str): String to decode. May also be Unicode.
-        encoding (str, optional): Encoding to use on bytestrings.
-        norm (None, optional): Normalisation form to apply to Unicode string.
-
-    Returns:
-        unicode: Decoded, optionally normalised, Unicode string.
-
-    """
-    if not isinstance(s, unicode):
-        s = unicode(s, encoding)
-
-    if norm:
-        from unicodedata import normalize
-        s = normalize(norm, s)
-
-    return s
-
-
-def utf8ify(s):
-    """Ensure string is a bytestring.
-
-    .. versionadded:: 1.31
-
-    Returns `str` objects unchanced, encodes `unicode` objects to
-    UTF-8, and calls :func:`str` on anything else.
-
-    Args:
-        s (object): A Python object
-
-    Returns:
-        str: UTF-8 string or string representation of s.
-
-    """
-    if isinstance(s, str):
-        return s
-
-    if isinstance(s, unicode):
-        return s.encode('utf-8')
-
-    return str(s)
-
-
-def applescriptify(s):
-    """Escape string for insertion into an AppleScript string.
-
-    .. versionadded:: 1.31
-
-    Replaces ``"`` with `"& quote &"`. Use this function if you want
-    to insert a string into an AppleScript script:
-
-        >>> applescriptify('g "python" test')
-        'g " & quote & "python" & quote & "test'
-
-    Args:
-        s (unicode): Unicode string to escape.
-
-    Returns:
-        unicode: Escaped string.
-
-    """
-    return s.replace(u'"', u'" & quote & "')
-
-
-def run_command(cmd, **kwargs):
-    """Run a command and return the output.
-
-    .. versionadded:: 1.31
-
-    A thin wrapper around :func:`subprocess.check_output` that ensures
-    all arguments are encoded to UTF-8 first.
-
-    Args:
-        cmd (list): Command arguments to pass to :func:`~subprocess.check_output`.
-        **kwargs: Keyword arguments to pass to :func:`~subprocess.check_output`.
-
-    Returns:
-        str: Output returned by :func:`~subprocess.check_output`.
-
-    """
-    cmd = [utf8ify(s) for s in cmd]
-    return subprocess.check_output(cmd, **kwargs)
-
-
-def run_applescript(script, *args, **kwargs):
-    """Execute an AppleScript script and return its output.
-
-    .. versionadded:: 1.31
-
-    Run AppleScript either by filepath or code. If ``script`` is a valid
-    filepath, that script will be run, otherwise ``script`` is treated
-    as code.
-
-    Args:
-        script (str, optional): Filepath of script or code to run.
-        *args: Optional command-line arguments to pass to the script.
-        **kwargs: Pass ``lang`` to run a language other than AppleScript.
-            Any other keyword arguments are passed to :func:`run_command`.
-
-    Returns:
-        str: Output of run command.
-
-    """
-    lang = 'AppleScript'
-    if 'lang' in kwargs:
-        lang = kwargs['lang']
-        del kwargs['lang']
-
-    cmd = ['/usr/bin/osascript', '-l', lang]
-
-    if os.path.exists(script):
-        cmd += [script]
-    else:
-        cmd += ['-e', script]
-
-    cmd.extend(args)
-
-    return run_command(cmd, **kwargs)
-
-
-def run_jxa(script, *args):
-    """Execute a JXA script and return its output.
-
-    .. versionadded:: 1.31
-
-    Wrapper around :func:`run_applescript` that passes ``lang=JavaScript``.
-
-    Args:
-        script (str): Filepath of script or code to run.
-        *args: Optional command-line arguments to pass to script.
-
-    Returns:
-        str: Output of script.
-
-    """
-    return run_applescript(script, *args, lang='JavaScript')
-
-
-def run_trigger(name, bundleid=None, arg=None):
-    """Call an Alfred External Trigger.
-
-    .. versionadded:: 1.31
-
-    If ``bundleid`` is not specified, the bundle ID of the calling
-    workflow is used.
-
-    Args:
-        name (str): Name of External Trigger to call.
-        bundleid (str, optional): Bundle ID of workflow trigger belongs to.
-        arg (str, optional): Argument to pass to trigger.
-
-    """
-    bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
-    appname = jxa_app_name()
-    opts = {'inWorkflow': bundleid}
-    if arg:
-        opts['withArgument'] = arg
-
-    script = JXA_TRIGGER.format(app=json.dumps(appname),
-                                arg=json.dumps(name),
-                                opts=json.dumps(opts, sort_keys=True))
-
-    run_applescript(script, lang='JavaScript')
-
-
-def set_theme(theme_name):
-    """Change Alfred's theme.
-
-    .. versionadded:: 1.39.0
-
-    Args:
-        theme_name (unicode): Name of theme Alfred should use.
-
-    """
-    appname = jxa_app_name()
-    script = JXA_SET_THEME.format(app=json.dumps(appname),
-                                  arg=json.dumps(theme_name))
-    run_applescript(script, lang='JavaScript')
-
-
-def set_config(name, value, bundleid=None, exportable=False):
-    """Set a workflow variable in ``info.plist``.
-
-    .. versionadded:: 1.33
-
-    If ``bundleid`` is not specified, the bundle ID of the calling
-    workflow is used.
-
-    Args:
-        name (str): Name of variable to set.
-        value (str): Value to set variable to.
-        bundleid (str, optional): Bundle ID of workflow variable belongs to.
-        exportable (bool, optional): Whether variable should be marked
-            as exportable (Don't Export checkbox).
-
-    """
-    bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
-    appname = jxa_app_name()
-    opts = {
-        'toValue': value,
-        'inWorkflow': bundleid,
-        'exportable': exportable,
-    }
-
-    script = JXA_SET_CONFIG.format(app=json.dumps(appname),
-                                   arg=json.dumps(name),
-                                   opts=json.dumps(opts, sort_keys=True))
-
-    run_applescript(script, lang='JavaScript')
-
-
-def unset_config(name, bundleid=None):
-    """Delete a workflow variable from ``info.plist``.
-
-    .. versionadded:: 1.33
-
-    If ``bundleid`` is not specified, the bundle ID of the calling
-    workflow is used.
-
-    Args:
-        name (str): Name of variable to delete.
-        bundleid (str, optional): Bundle ID of workflow variable belongs to.
-
-    """
-    bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
-    appname = jxa_app_name()
-    opts = {'inWorkflow': bundleid}
-
-    script = JXA_UNSET_CONFIG.format(app=json.dumps(appname),
-                                     arg=json.dumps(name),
-                                     opts=json.dumps(opts, sort_keys=True))
-
-    run_applescript(script, lang='JavaScript')
-
-
-def search_in_alfred(query=None):
-    """Open Alfred with given search query.
-
-    .. versionadded:: 1.39.0
-
-    Omit ``query`` to simply open Alfred's main window.
-
-    Args:
-        query (unicode, optional): Search query.
-
-    """
-    query = query or u''
-    appname = jxa_app_name()
-    script = JXA_SEARCH.format(app=json.dumps(appname), arg=json.dumps(query))
-    run_applescript(script, lang='JavaScript')
-
-
-def browse_in_alfred(path):
-    """Open Alfred's filesystem navigation mode at ``path``.
-
-    .. versionadded:: 1.39.0
-
-    Args:
-        path (unicode): File or directory path.
-
-    """
-    appname = jxa_app_name()
-    script = JXA_BROWSE.format(app=json.dumps(appname), arg=json.dumps(path))
-    run_applescript(script, lang='JavaScript')
-
-
-def action_in_alfred(paths):
-    """Action the give filepaths in Alfred.
-
-    .. versionadded:: 1.39.0
-
-    Args:
-        paths (list): Unicode paths to files/directories to action.
-
-    """
-    appname = jxa_app_name()
-    script = JXA_ACTION.format(app=json.dumps(appname), arg=json.dumps(paths))
-    run_applescript(script, lang='JavaScript')
-
-
-def reload_workflow(bundleid=None):
-    """Tell Alfred to reload a workflow from disk.
-
-    .. versionadded:: 1.39.0
-
-    If ``bundleid`` is not specified, the bundle ID of the calling
-    workflow is used.
-
-    Args:
-        bundleid (unicode, optional): Bundle ID of workflow to reload.
-
-    """
-    bundleid = bundleid or os.getenv('alfred_workflow_bundleid')
-    appname = jxa_app_name()
-    script = JXA_RELOAD_WORKFLOW.format(app=json.dumps(appname),
-                                        arg=json.dumps(bundleid))
-
-    run_applescript(script, lang='JavaScript')
-
-
-def appinfo(name):
-    """Get information about an installed application.
-
-    .. versionadded:: 1.31
-
-    Args:
-        name (str): Name of application to look up.
-
-    Returns:
-        AppInfo: :class:`AppInfo` tuple or ``None`` if app isn't found.
-
-    """
-    cmd = [
-        'mdfind',
-        '-onlyin', '/Applications',
-        '-onlyin', '/System/Applications',
-        '-onlyin', os.path.expanduser('~/Applications'),
-        '(kMDItemContentTypeTree == com.apple.application &&'
-        '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))'
-        .format(name)
-    ]
-
-    output = run_command(cmd).strip()
-    if not output:
-        return None
-
-    path = output.split('\n')[0]
-
-    cmd = ['mdls', '-raw', '-name', 'kMDItemCFBundleIdentifier', path]
-    bid = run_command(cmd).strip()
-    if not bid:  # pragma: no cover
-        return None
-
-    return AppInfo(unicodify(name), unicodify(path), unicodify(bid))
-
-
-@contextmanager
-def atomic_writer(fpath, mode):
-    """Atomic file writer.
-
-    .. versionadded:: 1.12
-
-    Context manager that ensures the file is only written if the write
-    succeeds. The data is first written to a temporary file.
-
-    :param fpath: path of file to write to.
-    :type fpath: ``unicode``
-    :param mode: sames as for :func:`open`
-    :type mode: string
-
-    """
-    suffix = '.{}.tmp'.format(os.getpid())
-    temppath = fpath + suffix
-    with open(temppath, mode) as fp:
-        try:
-            yield fp
-            os.rename(temppath, fpath)
-        finally:
-            try:
-                os.remove(temppath)
-            except (OSError, IOError):
-                pass
-
-
-class LockFile(object):
-    """Context manager to protect filepaths with lockfiles.
-
-    .. versionadded:: 1.13
-
-    Creates a lockfile alongside ``protected_path``. Other ``LockFile``
-    instances will refuse to lock the same path.
-
-    >>> path = '/path/to/file'
-    >>> with LockFile(path):
-    >>>     with open(path, 'wb') as fp:
-    >>>         fp.write(data)
-
-    Args:
-        protected_path (unicode): File to protect with a lockfile
-        timeout (float, optional): Raises an :class:`AcquisitionError`
-            if lock cannot be acquired within this number of seconds.
-            If ``timeout`` is 0 (the default), wait forever.
-        delay (float, optional): How often to check (in seconds) if
-            lock has been released.
-
-    Attributes:
-        delay (float): How often to check (in seconds) whether the lock
-            can be acquired.
-        lockfile (unicode): Path of the lockfile.
-        timeout (float): How long to wait to acquire the lock.
-
-    """
-
-    def __init__(self, protected_path, timeout=0.0, delay=0.05):
-        """Create new :class:`LockFile` object."""
-        self.lockfile = protected_path + '.lock'
-        self._lockfile = None
-        self.timeout = timeout
-        self.delay = delay
-        self._lock = Event()
-        atexit.register(self.release)
-
-    @property
-    def locked(self):
-        """``True`` if file is locked by this instance."""
-        return self._lock.is_set()
-
-    def acquire(self, blocking=True):
-        """Acquire the lock if possible.
-
-        If the lock is in use and ``blocking`` is ``False``, return
-        ``False``.
-
-        Otherwise, check every :attr:`delay` seconds until it acquires
-        lock or exceeds attr:`timeout` and raises an :class:`AcquisitionError`.
-
-        """
-        if self.locked and not blocking:
-            return False
-
-        start = time.time()
-        while True:
-            # Raise error if we've been waiting too long to acquire the lock
-            if self.timeout and (time.time() - start) >= self.timeout:
-                raise AcquisitionError('lock acquisition timed out')
-
-            # If already locked, wait then try again
-            if self.locked:
-                time.sleep(self.delay)
-                continue
-
-            # Create in append mode so we don't lose any contents
-            if self._lockfile is None:
-                self._lockfile = open(self.lockfile, 'a')
-
-            # Try to acquire the lock
-            try:
-                fcntl.lockf(self._lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB)
-                self._lock.set()
-                break
-            except IOError as err:  # pragma: no cover
-                if err.errno not in (errno.EACCES, errno.EAGAIN):
-                    raise
-
-                # Don't try again
-                if not blocking:  # pragma: no cover
-                    return False
-
-                # Wait, then try again
-                time.sleep(self.delay)
-
-        return True
-
-    def release(self):
-        """Release the lock by deleting `self.lockfile`."""
-        if not self._lock.is_set():
-            return False
-
-        try:
-            fcntl.lockf(self._lockfile, fcntl.LOCK_UN)
-        except IOError:  # pragma: no cover
-            pass
-        finally:
-            self._lock.clear()
-            self._lockfile = None
-            try:
-                os.unlink(self.lockfile)
-            except (IOError, OSError):  # pragma: no cover
-                pass
-
-            return True
-
-    def __enter__(self):
-        """Acquire lock."""
-        self.acquire()
-        return self
-
-    def __exit__(self, typ, value, traceback):
-        """Release lock."""
-        self.release()
-
-    def __del__(self):
-        """Clear up `self.lockfile`."""
-        self.release()  # pragma: no cover
-
-
-class uninterruptible(object):
-    """Decorator that postpones SIGTERM until wrapped function returns.
-
-    .. versionadded:: 1.12
-
-    .. important:: This decorator is NOT thread-safe.
-
-    As of version 2.7, Alfred allows Script Filters to be killed. If
-    your workflow is killed in the middle of critical code (e.g.
-    writing data to disk), this may corrupt your workflow's data.
-
-    Use this decorator to wrap critical functions that *must* complete.
-    If the script is killed while a wrapped function is executing,
-    the SIGTERM will be caught and handled after your function has
-    finished executing.
-
-    Alfred-Workflow uses this internally to ensure its settings, data
-    and cache writes complete.
-
-    """
-
-    def __init__(self, func, class_name=''):
-        """Decorate `func`."""
-        self.func = func
-        functools.update_wrapper(self, func)
-        self._caught_signal = None
-
-    def signal_handler(self, signum, frame):
-        """Called when process receives SIGTERM."""
-        self._caught_signal = (signum, frame)
-
-    def __call__(self, *args, **kwargs):
-        """Trap ``SIGTERM`` and call wrapped function."""
-        self._caught_signal = None
-        # Register handler for SIGTERM, then call `self.func`
-        self.old_signal_handler = signal.getsignal(signal.SIGTERM)
-        signal.signal(signal.SIGTERM, self.signal_handler)
-
-        self.func(*args, **kwargs)
-
-        # Restore old signal handler
-        signal.signal(signal.SIGTERM, self.old_signal_handler)
-
-        # Handle any signal caught during execution
-        if self._caught_signal is not None:
-            signum, frame = self._caught_signal
-            if callable(self.old_signal_handler):
-                self.old_signal_handler(signum, frame)
-            elif self.old_signal_handler == signal.SIG_DFL:
-                sys.exit(0)
-
-    def __get__(self, obj=None, klass=None):
-        """Decorator API."""
-        return self.__class__(self.func.__get__(obj, klass),
-                              klass.__name__)

+ 0 - 1
alfred/workflow/version

@@ -1 +0,0 @@
-1.39.0

+ 0 - 720
alfred/workflow/web.py

@@ -1,720 +0,0 @@
-# encoding: utf-8
-#
-# Copyright (c) 2014 Dean Jackson <[email protected]>
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2014-02-15
-#
-
-"""Lightweight HTTP library with a requests-like interface."""
-
-from __future__ import absolute_import, print_function
-
-import codecs
-import json
-import mimetypes
-import os
-import random
-import re
-import socket
-import string
-import unicodedata
-import urllib
-import urllib2
-import urlparse
-import zlib
-
-__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
-
-USER_AGENT = (u'Alfred-Workflow/' + __version__ +
-              ' (+http://www.deanishe.net/alfred-workflow)')
-
-# Valid characters for multipart form data boundaries
-BOUNDARY_CHARS = string.digits + string.ascii_letters
-
-# HTTP response codes
-RESPONSES = {
-    100: 'Continue',
-    101: 'Switching Protocols',
-    200: 'OK',
-    201: 'Created',
-    202: 'Accepted',
-    203: 'Non-Authoritative Information',
-    204: 'No Content',
-    205: 'Reset Content',
-    206: 'Partial Content',
-    300: 'Multiple Choices',
-    301: 'Moved Permanently',
-    302: 'Found',
-    303: 'See Other',
-    304: 'Not Modified',
-    305: 'Use Proxy',
-    307: 'Temporary Redirect',
-    400: 'Bad Request',
-    401: 'Unauthorized',
-    402: 'Payment Required',
-    403: 'Forbidden',
-    404: 'Not Found',
-    405: 'Method Not Allowed',
-    406: 'Not Acceptable',
-    407: 'Proxy Authentication Required',
-    408: 'Request Timeout',
-    409: 'Conflict',
-    410: 'Gone',
-    411: 'Length Required',
-    412: 'Precondition Failed',
-    413: 'Request Entity Too Large',
-    414: 'Request-URI Too Long',
-    415: 'Unsupported Media Type',
-    416: 'Requested Range Not Satisfiable',
-    417: 'Expectation Failed',
-    500: 'Internal Server Error',
-    501: 'Not Implemented',
-    502: 'Bad Gateway',
-    503: 'Service Unavailable',
-    504: 'Gateway Timeout',
-    505: 'HTTP Version Not Supported'
-}
-
-
-def str_dict(dic):
-    """Convert keys and values in ``dic`` into UTF-8-encoded :class:`str`.
-
-    :param dic: Mapping of Unicode strings
-    :type dic: dict
-    :returns: Dictionary containing only UTF-8 strings
-    :rtype: dict
-
-    """
-    if isinstance(dic, CaseInsensitiveDictionary):
-        dic2 = CaseInsensitiveDictionary()
-    else:
-        dic2 = {}
-    for k, v in dic.items():
-        if isinstance(k, unicode):
-            k = k.encode('utf-8')
-        if isinstance(v, unicode):
-            v = v.encode('utf-8')
-        dic2[k] = v
-    return dic2
-
-
-class NoRedirectHandler(urllib2.HTTPRedirectHandler):
-    """Prevent redirections."""
-
-    def redirect_request(self, *args):
-        """Ignore redirect."""
-        return None
-
-
-# Adapted from https://gist.github.com/babakness/3901174
-class CaseInsensitiveDictionary(dict):
-    """Dictionary with caseless key search.
-
-    Enables case insensitive searching while preserving case sensitivity
-    when keys are listed, ie, via keys() or items() methods.
-
-    Works by storing a lowercase version of the key as the new key and
-    stores the original key-value pair as the key's value
-    (values become dictionaries).
-
-    """
-
-    def __init__(self, initval=None):
-        """Create new case-insensitive dictionary."""
-        if isinstance(initval, dict):
-            for key, value in initval.iteritems():
-                self.__setitem__(key, value)
-
-        elif isinstance(initval, list):
-            for (key, value) in initval:
-                self.__setitem__(key, value)
-
-    def __contains__(self, key):
-        return dict.__contains__(self, key.lower())
-
-    def __getitem__(self, key):
-        return dict.__getitem__(self, key.lower())['val']
-
-    def __setitem__(self, key, value):
-        return dict.__setitem__(self, key.lower(), {'key': key, 'val': value})
-
-    def get(self, key, default=None):
-        """Return value for case-insensitive key or default."""
-        try:
-            v = dict.__getitem__(self, key.lower())
-        except KeyError:
-            return default
-        else:
-            return v['val']
-
-    def update(self, other):
-        """Update values from other ``dict``."""
-        for k, v in other.items():
-            self[k] = v
-
-    def items(self):
-        """Return ``(key, value)`` pairs."""
-        return [(v['key'], v['val']) for v in dict.itervalues(self)]
-
-    def keys(self):
-        """Return original keys."""
-        return [v['key'] for v in dict.itervalues(self)]
-
-    def values(self):
-        """Return all values."""
-        return [v['val'] for v in dict.itervalues(self)]
-
-    def iteritems(self):
-        """Iterate over ``(key, value)`` pairs."""
-        for v in dict.itervalues(self):
-            yield v['key'], v['val']
-
-    def iterkeys(self):
-        """Iterate over original keys."""
-        for v in dict.itervalues(self):
-            yield v['key']
-
-    def itervalues(self):
-        """Interate over values."""
-        for v in dict.itervalues(self):
-            yield v['val']
-
-
-class Request(urllib2.Request):
-    """Subclass of :class:`urllib2.Request` that supports custom methods."""
-
-    def __init__(self, *args, **kwargs):
-        """Create a new :class:`Request`."""
-        self._method = kwargs.pop('method', None)
-        urllib2.Request.__init__(self, *args, **kwargs)
-
-    def get_method(self):
-        return self._method.upper()
-
-
-class Response(object):
-    """
-    Returned by :func:`request` / :func:`get` / :func:`post` functions.
-
-    Simplified version of the ``Response`` object in the ``requests`` library.
-
-    >>> r = request('http://www.google.com')
-    >>> r.status_code
-    200
-    >>> r.encoding
-    ISO-8859-1
-    >>> r.content  # bytes
-    <html> ...
-    >>> r.text  # unicode, decoded according to charset in HTTP header/meta tag
-    u'<html> ...'
-    >>> r.json()  # content parsed as JSON
-
-    """
-
-    def __init__(self, request, stream=False):
-        """Call `request` with :mod:`urllib2` and process results.
-
-        :param request: :class:`Request` instance
-        :param stream: Whether to stream response or retrieve it all at once
-        :type stream: bool
-
-        """
-        self.request = request
-        self._stream = stream
-        self.url = None
-        self.raw = None
-        self._encoding = None
-        self.error = None
-        self.status_code = None
-        self.reason = None
-        self.headers = CaseInsensitiveDictionary()
-        self._content = None
-        self._content_loaded = False
-        self._gzipped = False
-
-        # Execute query
-        try:
-            self.raw = urllib2.urlopen(request)
-        except urllib2.HTTPError as err:
-            self.error = err
-            try:
-                self.url = err.geturl()
-            # sometimes (e.g. when authentication fails)
-            # urllib can't get a URL from an HTTPError
-            # This behaviour changes across Python versions,
-            # so no test cover (it isn't important).
-            except AttributeError:  # pragma: no cover
-                pass
-            self.status_code = err.code
-        else:
-            self.status_code = self.raw.getcode()
-            self.url = self.raw.geturl()
-        self.reason = RESPONSES.get(self.status_code)
-
-        # Parse additional info if request succeeded
-        if not self.error:
-            headers = self.raw.info()
-            self.transfer_encoding = headers.getencoding()
-            self.mimetype = headers.gettype()
-            for key in headers.keys():
-                self.headers[key.lower()] = headers.get(key)
-
-            # Is content gzipped?
-            # Transfer-Encoding appears to not be used in the wild
-            # (contrary to the HTTP standard), but no harm in testing
-            # for it
-            if 'gzip' in headers.get('content-encoding', '') or \
-                    'gzip' in headers.get('transfer-encoding', ''):
-                self._gzipped = True
-
-    @property
-    def stream(self):
-        """Whether response is streamed.
-
-        Returns:
-            bool: `True` if response is streamed.
-
-        """
-        return self._stream
-
-    @stream.setter
-    def stream(self, value):
-        if self._content_loaded:
-            raise RuntimeError("`content` has already been read from "
-                               "this Response.")
-
-        self._stream = value
-
-    def json(self):
-        """Decode response contents as JSON.
-
-        :returns: object decoded from JSON
-        :rtype: list, dict or unicode
-
-        """
-        return json.loads(self.content, self.encoding or 'utf-8')
-
-    @property
-    def encoding(self):
-        """Text encoding of document or ``None``.
-
-        :returns: Text encoding if found.
-        :rtype: str or ``None``
-
-        """
-        if not self._encoding:
-            self._encoding = self._get_encoding()
-
-        return self._encoding
-
-    @property
-    def content(self):
-        """Raw content of response (i.e. bytes).
-
-        :returns: Body of HTTP response
-        :rtype: str
-
-        """
-        if not self._content:
-
-            # Decompress gzipped content
-            if self._gzipped:
-                decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
-                self._content = decoder.decompress(self.raw.read())
-
-            else:
-                self._content = self.raw.read()
-
-            self._content_loaded = True
-
-        return self._content
-
-    @property
-    def text(self):
-        """Unicode-decoded content of response body.
-
-        If no encoding can be determined from HTTP headers or the content
-        itself, the encoded response body will be returned instead.
-
-        :returns: Body of HTTP response
-        :rtype: unicode or str
-
-        """
-        if self.encoding:
-            return unicodedata.normalize('NFC', unicode(self.content,
-                                                        self.encoding))
-        return self.content
-
-    def iter_content(self, chunk_size=4096, decode_unicode=False):
-        """Iterate over response data.
-
-        .. versionadded:: 1.6
-
-        :param chunk_size: Number of bytes to read into memory
-        :type chunk_size: int
-        :param decode_unicode: Decode to Unicode using detected encoding
-        :type decode_unicode: bool
-        :returns: iterator
-
-        """
-        if not self.stream:
-            raise RuntimeError("You cannot call `iter_content` on a "
-                               "Response unless you passed `stream=True`"
-                               " to `get()`/`post()`/`request()`.")
-
-        if self._content_loaded:
-            raise RuntimeError(
-                "`content` has already been read from this Response.")
-
-        def decode_stream(iterator, r):
-            dec = codecs.getincrementaldecoder(r.encoding)(errors='replace')
-
-            for chunk in iterator:
-                data = dec.decode(chunk)
-                if data:
-                    yield data
-
-            data = dec.decode(b'', final=True)
-            if data:  # pragma: no cover
-                yield data
-
-        def generate():
-            if self._gzipped:
-                decoder = zlib.decompressobj(16 + zlib.MAX_WBITS)
-
-            while True:
-                chunk = self.raw.read(chunk_size)
-                if not chunk:
-                    break
-
-                if self._gzipped:
-                    chunk = decoder.decompress(chunk)
-
-                yield chunk
-
-        chunks = generate()
-
-        if decode_unicode and self.encoding:
-            chunks = decode_stream(chunks, self)
-
-        return chunks
-
-    def save_to_path(self, filepath):
-        """Save retrieved data to file at ``filepath``.
-
-        .. versionadded: 1.9.6
-
-        :param filepath: Path to save retrieved data.
-
-        """
-        filepath = os.path.abspath(filepath)
-        dirname = os.path.dirname(filepath)
-        if not os.path.exists(dirname):
-            os.makedirs(dirname)
-
-        self.stream = True
-
-        with open(filepath, 'wb') as fileobj:
-            for data in self.iter_content():
-                fileobj.write(data)
-
-    def raise_for_status(self):
-        """Raise stored error if one occurred.
-
-        error will be instance of :class:`urllib2.HTTPError`
-        """
-        if self.error is not None:
-            raise self.error
-        return
-
-    def _get_encoding(self):
-        """Get encoding from HTTP headers or content.
-
-        :returns: encoding or `None`
-        :rtype: unicode or ``None``
-
-        """
-        headers = self.raw.info()
-        encoding = None
-
-        if headers.getparam('charset'):
-            encoding = headers.getparam('charset')
-
-        # HTTP Content-Type header
-        for param in headers.getplist():
-            if param.startswith('charset='):
-                encoding = param[8:]
-                break
-
-        if not self.stream:  # Try sniffing response content
-            # Encoding declared in document should override HTTP headers
-            if self.mimetype == 'text/html':  # sniff HTML headers
-                m = re.search(r"""<meta.+charset=["']{0,1}(.+?)["'].*>""",
-                              self.content)
-                if m:
-                    encoding = m.group(1)
-
-            elif ((self.mimetype.startswith('application/')
-                   or self.mimetype.startswith('text/'))
-                  and 'xml' in self.mimetype):
-                m = re.search(r"""<?xml.+encoding=["'](.+?)["'][^>]*\?>""",
-                              self.content)
-                if m:
-                    encoding = m.group(1)
-
-        # Format defaults
-        if self.mimetype == 'application/json' and not encoding:
-            # The default encoding for JSON
-            encoding = 'utf-8'
-
-        elif self.mimetype == 'application/xml' and not encoding:
-            # The default for 'application/xml'
-            encoding = 'utf-8'
-
-        if encoding:
-            encoding = encoding.lower()
-
-        return encoding
-
-
-def request(method, url, params=None, data=None, headers=None, cookies=None,
-            files=None, auth=None, timeout=60, allow_redirects=False,
-            stream=False):
-    """Initiate an HTTP(S) request. Returns :class:`Response` object.
-
-    :param method: 'GET' or 'POST'
-    :type method: unicode
-    :param url: URL to open
-    :type url: unicode
-    :param params: mapping of URL parameters
-    :type params: dict
-    :param data: mapping of form data ``{'field_name': 'value'}`` or
-        :class:`str`
-    :type data: dict or str
-    :param headers: HTTP headers
-    :type headers: dict
-    :param cookies: cookies to send to server
-    :type cookies: dict
-    :param files: files to upload (see below).
-    :type files: dict
-    :param auth: username, password
-    :type auth: tuple
-    :param timeout: connection timeout limit in seconds
-    :type timeout: int
-    :param allow_redirects: follow redirections
-    :type allow_redirects: bool
-    :param stream: Stream content instead of fetching it all at once.
-    :type stream: bool
-    :returns: Response object
-    :rtype: :class:`Response`
-
-
-    The ``files`` argument is a dictionary::
-
-        {'fieldname' : { 'filename': 'blah.txt',
-                         'content': '<binary data>',
-                         'mimetype': 'text/plain'}
-        }
-
-    * ``fieldname`` is the name of the field in the HTML form.
-    * ``mimetype`` is optional. If not provided, :mod:`mimetypes` will
-      be used to guess the mimetype, or ``application/octet-stream``
-      will be used.
-
-    """
-    # TODO: cookies
-    socket.setdefaulttimeout(timeout)
-
-    # Default handlers
-    openers = []
-
-    if not allow_redirects:
-        openers.append(NoRedirectHandler())
-
-    if auth is not None:  # Add authorisation handler
-        username, password = auth
-        password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
-        password_manager.add_password(None, url, username, password)
-        auth_manager = urllib2.HTTPBasicAuthHandler(password_manager)
-        openers.append(auth_manager)
-
-    # Install our custom chain of openers
-    opener = urllib2.build_opener(*openers)
-    urllib2.install_opener(opener)
-
-    if not headers:
-        headers = CaseInsensitiveDictionary()
-    else:
-        headers = CaseInsensitiveDictionary(headers)
-
-    if 'user-agent' not in headers:
-        headers['user-agent'] = USER_AGENT
-
-    # Accept gzip-encoded content
-    encodings = [s.strip() for s in
-                 headers.get('accept-encoding', '').split(',')]
-    if 'gzip' not in encodings:
-        encodings.append('gzip')
-
-    headers['accept-encoding'] = ', '.join(encodings)
-
-    if files:
-        if not data:
-            data = {}
-        new_headers, data = encode_multipart_formdata(data, files)
-        headers.update(new_headers)
-    elif data and isinstance(data, dict):
-        data = urllib.urlencode(str_dict(data))
-
-    # Make sure everything is encoded text
-    headers = str_dict(headers)
-
-    if isinstance(url, unicode):
-        url = url.encode('utf-8')
-
-    if params:  # GET args (POST args are handled in encode_multipart_formdata)
-
-        scheme, netloc, path, query, fragment = urlparse.urlsplit(url)
-
-        if query:  # Combine query string and `params`
-            url_params = urlparse.parse_qs(query)
-            # `params` take precedence over URL query string
-            url_params.update(params)
-            params = url_params
-
-        query = urllib.urlencode(str_dict(params), doseq=True)
-        url = urlparse.urlunsplit((scheme, netloc, path, query, fragment))
-
-    req = Request(url, data, headers, method=method)
-    return Response(req, stream)
-
-
-def get(url, params=None, headers=None, cookies=None, auth=None,
-        timeout=60, allow_redirects=True, stream=False):
-    """Initiate a GET request. Arguments as for :func:`request`.
-
-    :returns: :class:`Response` instance
-
-    """
-    return request('GET', url, params, headers=headers, cookies=cookies,
-                   auth=auth, timeout=timeout, allow_redirects=allow_redirects,
-                   stream=stream)
-
-
-def delete(url, params=None, data=None, headers=None, cookies=None, auth=None,
-           timeout=60, allow_redirects=True, stream=False):
-    """Initiate a DELETE request. Arguments as for :func:`request`.
-
-    :returns: :class:`Response` instance
-
-    """
-    return request('DELETE', url, params, data, headers=headers,
-                   cookies=cookies, auth=auth, timeout=timeout,
-                   allow_redirects=allow_redirects, stream=stream)
-
-
-def post(url, params=None, data=None, headers=None, cookies=None, files=None,
-         auth=None, timeout=60, allow_redirects=False, stream=False):
-    """Initiate a POST request. Arguments as for :func:`request`.
-
-    :returns: :class:`Response` instance
-
-    """
-    return request('POST', url, params, data, headers, cookies, files, auth,
-                   timeout, allow_redirects, stream)
-
-
-def put(url, params=None, data=None, headers=None, cookies=None, files=None,
-        auth=None, timeout=60, allow_redirects=False, stream=False):
-    """Initiate a PUT request. Arguments as for :func:`request`.
-
-    :returns: :class:`Response` instance
-
-    """
-    return request('PUT', url, params, data, headers, cookies, files, auth,
-                   timeout, allow_redirects, stream)
-
-
-def encode_multipart_formdata(fields, files):
-    """Encode form data (``fields``) and ``files`` for POST request.
-
-    :param fields: mapping of ``{name : value}`` pairs for normal form fields.
-    :type fields: dict
-    :param files: dictionary of fieldnames/files elements for file data.
-                  See below for details.
-    :type files: dict of :class:`dict`
-    :returns: ``(headers, body)`` ``headers`` is a
-        :class:`dict` of HTTP headers
-    :rtype: 2-tuple ``(dict, str)``
-
-    The ``files`` argument is a dictionary::
-
-        {'fieldname' : { 'filename': 'blah.txt',
-                         'content': '<binary data>',
-                         'mimetype': 'text/plain'}
-        }
-
-    - ``fieldname`` is the name of the field in the HTML form.
-    - ``mimetype`` is optional. If not provided, :mod:`mimetypes` will
-      be used to guess the mimetype, or ``application/octet-stream``
-      will be used.
-
-    """
-    def get_content_type(filename):
-        """Return or guess mimetype of ``filename``.
-
-        :param filename: filename of file
-        :type filename: unicode/str
-        :returns: mime-type, e.g. ``text/html``
-        :rtype: str
-
-        """
-        return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
-
-    boundary = '-----' + ''.join(random.choice(BOUNDARY_CHARS)
-                                 for i in range(30))
-    CRLF = '\r\n'
-    output = []
-
-    # Normal form fields
-    for (name, value) in fields.items():
-        if isinstance(name, unicode):
-            name = name.encode('utf-8')
-        if isinstance(value, unicode):
-            value = value.encode('utf-8')
-        output.append('--' + boundary)
-        output.append('Content-Disposition: form-data; name="%s"' % name)
-        output.append('')
-        output.append(value)
-
-    # Files to upload
-    for name, d in files.items():
-        filename = d[u'filename']
-        content = d[u'content']
-        if u'mimetype' in d:
-            mimetype = d[u'mimetype']
-        else:
-            mimetype = get_content_type(filename)
-        if isinstance(name, unicode):
-            name = name.encode('utf-8')
-        if isinstance(filename, unicode):
-            filename = filename.encode('utf-8')
-        if isinstance(mimetype, unicode):
-            mimetype = mimetype.encode('utf-8')
-        output.append('--' + boundary)
-        output.append('Content-Disposition: form-data; '
-                      'name="%s"; filename="%s"' % (name, filename))
-        output.append('Content-Type: %s' % mimetype)
-        output.append('')
-        output.append(content)
-
-    output.append('--' + boundary + '--')
-    output.append('')
-    body = CRLF.join(output)
-    headers = {
-        'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
-        'Content-Length': str(len(body)),
-    }
-    return (headers, body)

+ 0 - 2820
alfred/workflow/workflow.py

@@ -1,2820 +0,0 @@
-# encoding: utf-8
-#
-# Copyright (c) 2014 Dean Jackson <[email protected]>
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2014-02-15
-#
-
-"""The :class:`Workflow` object is the main interface to this library.
-
-:class:`Workflow` is targeted at Alfred 2. Use
-:class:`~workflow.Workflow3` if you want to use Alfred 3's new
-features, such as :ref:`workflow variables <workflow-variables>` or
-more powerful modifiers.
-
-See :ref:`setup` in the :ref:`user-manual` for an example of how to set
-up your Python script to best utilise the :class:`Workflow` object.
-
-"""
-
-from __future__ import print_function, unicode_literals
-
-import binascii
-import cPickle
-from copy import deepcopy
-import json
-import logging
-import logging.handlers
-import os
-import pickle
-import plistlib
-import re
-import shutil
-import string
-import subprocess
-import sys
-import time
-import unicodedata
-
-try:
-    import xml.etree.cElementTree as ET
-except ImportError:  # pragma: no cover
-    import xml.etree.ElementTree as ET
-
-# imported to maintain API
-from util import AcquisitionError  # noqa: F401
-from util import (
-    atomic_writer,
-    LockFile,
-    uninterruptible,
-)
-
-#: Sentinel for properties that haven't been set yet (that might
-#: correctly have the value ``None``)
-UNSET = object()
-
-####################################################################
-# Standard system icons
-####################################################################
-
-# These icons are default macOS icons. They are super-high quality, and
-# will be familiar to users.
-# This library uses `ICON_ERROR` when a workflow dies in flames, so
-# in my own workflows, I use `ICON_WARNING` for less fatal errors
-# (e.g. bad user input, no results etc.)
-
-# The system icons are all in this directory. There are many more than
-# are listed here
-
-ICON_ROOT = '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources'
-
-ICON_ACCOUNT = os.path.join(ICON_ROOT, 'Accounts.icns')
-ICON_BURN = os.path.join(ICON_ROOT, 'BurningIcon.icns')
-ICON_CLOCK = os.path.join(ICON_ROOT, 'Clock.icns')
-ICON_COLOR = os.path.join(ICON_ROOT, 'ProfileBackgroundColor.icns')
-ICON_COLOUR = ICON_COLOR  # Queen's English, if you please
-ICON_EJECT = os.path.join(ICON_ROOT, 'EjectMediaIcon.icns')
-# Shown when a workflow throws an error
-ICON_ERROR = os.path.join(ICON_ROOT, 'AlertStopIcon.icns')
-ICON_FAVORITE = os.path.join(ICON_ROOT, 'ToolbarFavoritesIcon.icns')
-ICON_FAVOURITE = ICON_FAVORITE
-ICON_GROUP = os.path.join(ICON_ROOT, 'GroupIcon.icns')
-ICON_HELP = os.path.join(ICON_ROOT, 'HelpIcon.icns')
-ICON_HOME = os.path.join(ICON_ROOT, 'HomeFolderIcon.icns')
-ICON_INFO = os.path.join(ICON_ROOT, 'ToolbarInfo.icns')
-ICON_NETWORK = os.path.join(ICON_ROOT, 'GenericNetworkIcon.icns')
-ICON_NOTE = os.path.join(ICON_ROOT, 'AlertNoteIcon.icns')
-ICON_SETTINGS = os.path.join(ICON_ROOT, 'ToolbarAdvanced.icns')
-ICON_SWIRL = os.path.join(ICON_ROOT, 'ErasingIcon.icns')
-ICON_SWITCH = os.path.join(ICON_ROOT, 'General.icns')
-ICON_SYNC = os.path.join(ICON_ROOT, 'Sync.icns')
-ICON_TRASH = os.path.join(ICON_ROOT, 'TrashIcon.icns')
-ICON_USER = os.path.join(ICON_ROOT, 'UserIcon.icns')
-ICON_WARNING = os.path.join(ICON_ROOT, 'AlertCautionIcon.icns')
-ICON_WEB = os.path.join(ICON_ROOT, 'BookmarkIcon.icns')
-
-####################################################################
-# non-ASCII to ASCII diacritic folding.
-# Used by `fold_to_ascii` method
-####################################################################
-
-ASCII_REPLACEMENTS = {
-    'À': 'A',
-    'Á': 'A',
-    'Â': 'A',
-    'Ã': 'A',
-    'Ä': 'A',
-    'Å': 'A',
-    'Æ': 'AE',
-    'Ç': 'C',
-    'È': 'E',
-    'É': 'E',
-    'Ê': 'E',
-    'Ë': 'E',
-    'Ì': 'I',
-    'Í': 'I',
-    'Î': 'I',
-    'Ï': 'I',
-    'Ð': 'D',
-    'Ñ': 'N',
-    'Ò': 'O',
-    'Ó': 'O',
-    'Ô': 'O',
-    'Õ': 'O',
-    'Ö': 'O',
-    'Ø': 'O',
-    'Ù': 'U',
-    'Ú': 'U',
-    'Û': 'U',
-    'Ü': 'U',
-    'Ý': 'Y',
-    'Þ': 'Th',
-    'ß': 'ss',
-    'à': 'a',
-    'á': 'a',
-    'â': 'a',
-    'ã': 'a',
-    'ä': 'a',
-    'å': 'a',
-    'æ': 'ae',
-    'ç': 'c',
-    'è': 'e',
-    'é': 'e',
-    'ê': 'e',
-    'ë': 'e',
-    'ì': 'i',
-    'í': 'i',
-    'î': 'i',
-    'ï': 'i',
-    'ð': 'd',
-    'ñ': 'n',
-    'ò': 'o',
-    'ó': 'o',
-    'ô': 'o',
-    'õ': 'o',
-    'ö': 'o',
-    'ø': 'o',
-    'ù': 'u',
-    'ú': 'u',
-    'û': 'u',
-    'ü': 'u',
-    'ý': 'y',
-    'þ': 'th',
-    'ÿ': 'y',
-    'Ł': 'L',
-    'ł': 'l',
-    'Ń': 'N',
-    'ń': 'n',
-    'Ņ': 'N',
-    'ņ': 'n',
-    'Ň': 'N',
-    'ň': 'n',
-    'Ŋ': 'ng',
-    'ŋ': 'NG',
-    'Ō': 'O',
-    'ō': 'o',
-    'Ŏ': 'O',
-    'ŏ': 'o',
-    'Ő': 'O',
-    'ő': 'o',
-    'Œ': 'OE',
-    'œ': 'oe',
-    'Ŕ': 'R',
-    'ŕ': 'r',
-    'Ŗ': 'R',
-    'ŗ': 'r',
-    'Ř': 'R',
-    'ř': 'r',
-    'Ś': 'S',
-    'ś': 's',
-    'Ŝ': 'S',
-    'ŝ': 's',
-    'Ş': 'S',
-    'ş': 's',
-    'Š': 'S',
-    'š': 's',
-    'Ţ': 'T',
-    'ţ': 't',
-    'Ť': 'T',
-    'ť': 't',
-    'Ŧ': 'T',
-    'ŧ': 't',
-    'Ũ': 'U',
-    'ũ': 'u',
-    'Ū': 'U',
-    'ū': 'u',
-    'Ŭ': 'U',
-    'ŭ': 'u',
-    'Ů': 'U',
-    'ů': 'u',
-    'Ű': 'U',
-    'ű': 'u',
-    'Ŵ': 'W',
-    'ŵ': 'w',
-    'Ŷ': 'Y',
-    'ŷ': 'y',
-    'Ÿ': 'Y',
-    'Ź': 'Z',
-    'ź': 'z',
-    'Ż': 'Z',
-    'ż': 'z',
-    'Ž': 'Z',
-    'ž': 'z',
-    'ſ': 's',
-    'Α': 'A',
-    'Β': 'B',
-    'Γ': 'G',
-    'Δ': 'D',
-    'Ε': 'E',
-    'Ζ': 'Z',
-    'Η': 'E',
-    'Θ': 'Th',
-    'Ι': 'I',
-    'Κ': 'K',
-    'Λ': 'L',
-    'Μ': 'M',
-    'Ν': 'N',
-    'Ξ': 'Ks',
-    'Ο': 'O',
-    'Π': 'P',
-    'Ρ': 'R',
-    'Σ': 'S',
-    'Τ': 'T',
-    'Υ': 'U',
-    'Φ': 'Ph',
-    'Χ': 'Kh',
-    'Ψ': 'Ps',
-    'Ω': 'O',
-    'α': 'a',
-    'β': 'b',
-    'γ': 'g',
-    'δ': 'd',
-    'ε': 'e',
-    'ζ': 'z',
-    'η': 'e',
-    'θ': 'th',
-    'ι': 'i',
-    'κ': 'k',
-    'λ': 'l',
-    'μ': 'm',
-    'ν': 'n',
-    'ξ': 'x',
-    'ο': 'o',
-    'π': 'p',
-    'ρ': 'r',
-    'ς': 's',
-    'σ': 's',
-    'τ': 't',
-    'υ': 'u',
-    'φ': 'ph',
-    'χ': 'kh',
-    'ψ': 'ps',
-    'ω': 'o',
-    'А': 'A',
-    'Б': 'B',
-    'В': 'V',
-    'Г': 'G',
-    'Д': 'D',
-    'Е': 'E',
-    'Ж': 'Zh',
-    'З': 'Z',
-    'И': 'I',
-    'Й': 'I',
-    'К': 'K',
-    'Л': 'L',
-    'М': 'M',
-    'Н': 'N',
-    'О': 'O',
-    'П': 'P',
-    'Р': 'R',
-    'С': 'S',
-    'Т': 'T',
-    'У': 'U',
-    'Ф': 'F',
-    'Х': 'Kh',
-    'Ц': 'Ts',
-    'Ч': 'Ch',
-    'Ш': 'Sh',
-    'Щ': 'Shch',
-    'Ъ': "'",
-    'Ы': 'Y',
-    'Ь': "'",
-    'Э': 'E',
-    'Ю': 'Iu',
-    'Я': 'Ia',
-    'а': 'a',
-    'б': 'b',
-    'в': 'v',
-    'г': 'g',
-    'д': 'd',
-    'е': 'e',
-    'ж': 'zh',
-    'з': 'z',
-    'и': 'i',
-    'й': 'i',
-    'к': 'k',
-    'л': 'l',
-    'м': 'm',
-    'н': 'n',
-    'о': 'o',
-    'п': 'p',
-    'р': 'r',
-    'с': 's',
-    'т': 't',
-    'у': 'u',
-    'ф': 'f',
-    'х': 'kh',
-    'ц': 'ts',
-    'ч': 'ch',
-    'ш': 'sh',
-    'щ': 'shch',
-    'ъ': "'",
-    'ы': 'y',
-    'ь': "'",
-    'э': 'e',
-    'ю': 'iu',
-    'я': 'ia',
-    # 'ᴀ': '',
-    # 'ᴁ': '',
-    # 'ᴂ': '',
-    # 'ᴃ': '',
-    # 'ᴄ': '',
-    # 'ᴅ': '',
-    # 'ᴆ': '',
-    # 'ᴇ': '',
-    # 'ᴈ': '',
-    # 'ᴉ': '',
-    # 'ᴊ': '',
-    # 'ᴋ': '',
-    # 'ᴌ': '',
-    # 'ᴍ': '',
-    # 'ᴎ': '',
-    # 'ᴏ': '',
-    # 'ᴐ': '',
-    # 'ᴑ': '',
-    # 'ᴒ': '',
-    # 'ᴓ': '',
-    # 'ᴔ': '',
-    # 'ᴕ': '',
-    # 'ᴖ': '',
-    # 'ᴗ': '',
-    # 'ᴘ': '',
-    # 'ᴙ': '',
-    # 'ᴚ': '',
-    # 'ᴛ': '',
-    # 'ᴜ': '',
-    # 'ᴝ': '',
-    # 'ᴞ': '',
-    # 'ᴟ': '',
-    # 'ᴠ': '',
-    # 'ᴡ': '',
-    # 'ᴢ': '',
-    # 'ᴣ': '',
-    # 'ᴤ': '',
-    # 'ᴥ': '',
-    'ᴦ': 'G',
-    'ᴧ': 'L',
-    'ᴨ': 'P',
-    'ᴩ': 'R',
-    'ᴪ': 'PS',
-    'ẞ': 'Ss',
-    'Ỳ': 'Y',
-    'ỳ': 'y',
-    'Ỵ': 'Y',
-    'ỵ': 'y',
-    'Ỹ': 'Y',
-    'ỹ': 'y',
-}
-
-####################################################################
-# Smart-to-dumb punctuation mapping
-####################################################################
-
-DUMB_PUNCTUATION = {
-    '‘': "'",
-    '’': "'",
-    '‚': "'",
-    '“': '"',
-    '”': '"',
-    '„': '"',
-    '–': '-',
-    '—': '-'
-}
-
-
-####################################################################
-# Used by `Workflow.filter`
-####################################################################
-
-# Anchor characters in a name
-#: Characters that indicate the beginning of a "word" in CamelCase
-INITIALS = string.ascii_uppercase + string.digits
-
-#: Split on non-letters, numbers
-split_on_delimiters = re.compile('[^a-zA-Z0-9]').split
-
-# Match filter flags
-#: Match items that start with ``query``
-MATCH_STARTSWITH = 1
-#: Match items whose capital letters start with ``query``
-MATCH_CAPITALS = 2
-#: Match items with a component "word" that matches ``query``
-MATCH_ATOM = 4
-#: Match items whose initials (based on atoms) start with ``query``
-MATCH_INITIALS_STARTSWITH = 8
-#: Match items whose initials (based on atoms) contain ``query``
-MATCH_INITIALS_CONTAIN = 16
-#: Combination of :const:`MATCH_INITIALS_STARTSWITH` and
-#: :const:`MATCH_INITIALS_CONTAIN`
-MATCH_INITIALS = 24
-#: Match items if ``query`` is a substring
-MATCH_SUBSTRING = 32
-#: Match items if all characters in ``query`` appear in the item in order
-MATCH_ALLCHARS = 64
-#: Combination of all other ``MATCH_*`` constants
-MATCH_ALL = 127
-
-
-####################################################################
-# Used by `Workflow.check_update`
-####################################################################
-
-# Number of days to wait between checking for updates to the workflow
-DEFAULT_UPDATE_FREQUENCY = 1
-
-
-####################################################################
-# Keychain access errors
-####################################################################
-
-
-class KeychainError(Exception):
-    """Raised for unknown Keychain errors.
-
-    Raised by methods :meth:`Workflow.save_password`,
-    :meth:`Workflow.get_password` and :meth:`Workflow.delete_password`
-    when ``security`` CLI app returns an unknown error code.
-
-    """
-
-
-class PasswordNotFound(KeychainError):
-    """Password not in Keychain.
-
-    Raised by method :meth:`Workflow.get_password` when ``account``
-    is unknown to the Keychain.
-
-    """
-
-
-class PasswordExists(KeychainError):
-    """Raised when trying to overwrite an existing account password.
-
-    You should never receive this error: it is used internally
-    by the :meth:`Workflow.save_password` method to know if it needs
-    to delete the old password first (a Keychain implementation detail).
-
-    """
-
-
-####################################################################
-# Helper functions
-####################################################################
-
-def isascii(text):
-    """Test if ``text`` contains only ASCII characters.
-
-    :param text: text to test for ASCII-ness
-    :type text: ``unicode``
-    :returns: ``True`` if ``text`` contains only ASCII characters
-    :rtype: ``Boolean``
-
-    """
-    try:
-        text.encode('ascii')
-    except UnicodeEncodeError:
-        return False
-    return True
-
-
-####################################################################
-# Implementation classes
-####################################################################
-
-class SerializerManager(object):
-    """Contains registered serializers.
-
-    .. versionadded:: 1.8
-
-    A configured instance of this class is available at
-    :attr:`workflow.manager`.
-
-    Use :meth:`register()` to register new (or replace
-    existing) serializers, which you can specify by name when calling
-    :class:`~workflow.Workflow` data storage methods.
-
-    See :ref:`guide-serialization` and :ref:`guide-persistent-data`
-    for further information.
-
-    """
-
-    def __init__(self):
-        """Create new SerializerManager object."""
-        self._serializers = {}
-
-    def register(self, name, serializer):
-        """Register ``serializer`` object under ``name``.
-
-        Raises :class:`AttributeError` if ``serializer`` in invalid.
-
-        .. note::
-
-            ``name`` will be used as the file extension of the saved files.
-
-        :param name: Name to register ``serializer`` under
-        :type name: ``unicode`` or ``str``
-        :param serializer: object with ``load()`` and ``dump()``
-            methods
-
-        """
-        # Basic validation
-        getattr(serializer, 'load')
-        getattr(serializer, 'dump')
-
-        self._serializers[name] = serializer
-
-    def serializer(self, name):
-        """Return serializer object for ``name``.
-
-        :param name: Name of serializer to return
-        :type name: ``unicode`` or ``str``
-        :returns: serializer object or ``None`` if no such serializer
-            is registered.
-
-        """
-        return self._serializers.get(name)
-
-    def unregister(self, name):
-        """Remove registered serializer with ``name``.
-
-        Raises a :class:`ValueError` if there is no such registered
-        serializer.
-
-        :param name: Name of serializer to remove
-        :type name: ``unicode`` or ``str``
-        :returns: serializer object
-
-        """
-        if name not in self._serializers:
-            raise ValueError('No such serializer registered : {0}'.format(
-                             name))
-
-        serializer = self._serializers[name]
-        del self._serializers[name]
-
-        return serializer
-
-    @property
-    def serializers(self):
-        """Return names of registered serializers."""
-        return sorted(self._serializers.keys())
-
-
-class JSONSerializer(object):
-    """Wrapper around :mod:`json`. Sets ``indent`` and ``encoding``.
-
-    .. versionadded:: 1.8
-
-    Use this serializer if you need readable data files. JSON doesn't
-    support Python objects as well as ``cPickle``/``pickle``, so be
-    careful which data you try to serialize as JSON.
-
-    """
-
-    @classmethod
-    def load(cls, file_obj):
-        """Load serialized object from open JSON file.
-
-        .. versionadded:: 1.8
-
-        :param file_obj: file handle
-        :type file_obj: ``file`` object
-        :returns: object loaded from JSON file
-        :rtype: object
-
-        """
-        return json.load(file_obj)
-
-    @classmethod
-    def dump(cls, obj, file_obj):
-        """Serialize object ``obj`` to open JSON file.
-
-        .. versionadded:: 1.8
-
-        :param obj: Python object to serialize
-        :type obj: JSON-serializable data structure
-        :param file_obj: file handle
-        :type file_obj: ``file`` object
-
-        """
-        return json.dump(obj, file_obj, indent=2, encoding='utf-8')
-
-
-class CPickleSerializer(object):
-    """Wrapper around :mod:`cPickle`. Sets ``protocol``.
-
-    .. versionadded:: 1.8
-
-    This is the default serializer and the best combination of speed and
-    flexibility.
-
-    """
-
-    @classmethod
-    def load(cls, file_obj):
-        """Load serialized object from open pickle file.
-
-        .. versionadded:: 1.8
-
-        :param file_obj: file handle
-        :type file_obj: ``file`` object
-        :returns: object loaded from pickle file
-        :rtype: object
-
-        """
-        return cPickle.load(file_obj)
-
-    @classmethod
-    def dump(cls, obj, file_obj):
-        """Serialize object ``obj`` to open pickle file.
-
-        .. versionadded:: 1.8
-
-        :param obj: Python object to serialize
-        :type obj: Python object
-        :param file_obj: file handle
-        :type file_obj: ``file`` object
-
-        """
-        return cPickle.dump(obj, file_obj, protocol=-1)
-
-
-class PickleSerializer(object):
-    """Wrapper around :mod:`pickle`. Sets ``protocol``.
-
-    .. versionadded:: 1.8
-
-    Use this serializer if you need to add custom pickling.
-
-    """
-
-    @classmethod
-    def load(cls, file_obj):
-        """Load serialized object from open pickle file.
-
-        .. versionadded:: 1.8
-
-        :param file_obj: file handle
-        :type file_obj: ``file`` object
-        :returns: object loaded from pickle file
-        :rtype: object
-
-        """
-        return pickle.load(file_obj)
-
-    @classmethod
-    def dump(cls, obj, file_obj):
-        """Serialize object ``obj`` to open pickle file.
-
-        .. versionadded:: 1.8
-
-        :param obj: Python object to serialize
-        :type obj: Python object
-        :param file_obj: file handle
-        :type file_obj: ``file`` object
-
-        """
-        return pickle.dump(obj, file_obj, protocol=-1)
-
-
-# Set up default manager and register built-in serializers
-manager = SerializerManager()
-manager.register('cpickle', CPickleSerializer)
-manager.register('pickle', PickleSerializer)
-manager.register('json', JSONSerializer)
-
-
-class Item(object):
-    """Represents a feedback item for Alfred.
-
-    Generates Alfred-compliant XML for a single item.
-
-    You probably shouldn't use this class directly, but via
-    :meth:`Workflow.add_item`. See :meth:`~Workflow.add_item`
-    for details of arguments.
-
-    """
-
-    def __init__(self, title, subtitle='', modifier_subtitles=None,
-                 arg=None, autocomplete=None, valid=False, uid=None,
-                 icon=None, icontype=None, type=None, largetext=None,
-                 copytext=None, quicklookurl=None):
-        """Same arguments as :meth:`Workflow.add_item`."""
-        self.title = title
-        self.subtitle = subtitle
-        self.modifier_subtitles = modifier_subtitles or {}
-        self.arg = arg
-        self.autocomplete = autocomplete
-        self.valid = valid
-        self.uid = uid
-        self.icon = icon
-        self.icontype = icontype
-        self.type = type
-        self.largetext = largetext
-        self.copytext = copytext
-        self.quicklookurl = quicklookurl
-
-    @property
-    def elem(self):
-        """Create and return feedback item for Alfred.
-
-        :returns: :class:`ElementTree.Element <xml.etree.ElementTree.Element>`
-            instance for this :class:`Item` instance.
-
-        """
-        # Attributes on <item> element
-        attr = {}
-        if self.valid:
-            attr['valid'] = 'yes'
-        else:
-            attr['valid'] = 'no'
-        # Allow empty string for autocomplete. This is a useful value,
-        # as TABing the result will revert the query back to just the
-        # keyword
-        if self.autocomplete is not None:
-            attr['autocomplete'] = self.autocomplete
-
-        # Optional attributes
-        for name in ('uid', 'type'):
-            value = getattr(self, name, None)
-            if value:
-                attr[name] = value
-
-        root = ET.Element('item', attr)
-        ET.SubElement(root, 'title').text = self.title
-        ET.SubElement(root, 'subtitle').text = self.subtitle
-
-        # Add modifier subtitles
-        for mod in ('cmd', 'ctrl', 'alt', 'shift', 'fn'):
-            if mod in self.modifier_subtitles:
-                ET.SubElement(root, 'subtitle',
-                              {'mod': mod}).text = self.modifier_subtitles[mod]
-
-        # Add arg as element instead of attribute on <item>, as it's more
-        # flexible (newlines aren't allowed in attributes)
-        if self.arg:
-            ET.SubElement(root, 'arg').text = self.arg
-
-        # Add icon if there is one
-        if self.icon:
-            if self.icontype:
-                attr = dict(type=self.icontype)
-            else:
-                attr = {}
-            ET.SubElement(root, 'icon', attr).text = self.icon
-
-        if self.largetext:
-            ET.SubElement(root, 'text',
-                          {'type': 'largetype'}).text = self.largetext
-
-        if self.copytext:
-            ET.SubElement(root, 'text',
-                          {'type': 'copy'}).text = self.copytext
-
-        if self.quicklookurl:
-            ET.SubElement(root, 'quicklookurl').text = self.quicklookurl
-
-        return root
-
-
-class Settings(dict):
-    """A dictionary that saves itself when changed.
-
-    Dictionary keys & values will be saved as a JSON file
-    at ``filepath``. If the file does not exist, the dictionary
-    (and settings file) will be initialised with ``defaults``.
-
-    :param filepath: where to save the settings
-    :type filepath: :class:`unicode`
-    :param defaults: dict of default settings
-    :type defaults: :class:`dict`
-
-
-    An appropriate instance is provided by :class:`Workflow` instances at
-    :attr:`Workflow.settings`.
-
-    """
-
-    def __init__(self, filepath, defaults=None):
-        """Create new :class:`Settings` object."""
-        super(Settings, self).__init__()
-        self._filepath = filepath
-        self._nosave = False
-        self._original = {}
-        if os.path.exists(self._filepath):
-            self._load()
-        elif defaults:
-            for key, val in defaults.items():
-                self[key] = val
-            self.save()  # save default settings
-
-    def _load(self):
-        """Load cached settings from JSON file `self._filepath`."""
-        data = {}
-        with LockFile(self._filepath, 0.5):
-            with open(self._filepath, 'rb') as fp:
-                data.update(json.load(fp))
-
-        self._original = deepcopy(data)
-
-        self._nosave = True
-        self.update(data)
-        self._nosave = False
-
-    @uninterruptible
-    def save(self):
-        """Save settings to JSON file specified in ``self._filepath``.
-
-        If you're using this class via :attr:`Workflow.settings`, which
-        you probably are, ``self._filepath`` will be ``settings.json``
-        in your workflow's data directory (see :attr:`~Workflow.datadir`).
-        """
-        if self._nosave:
-            return
-
-        data = {}
-        data.update(self)
-
-        with LockFile(self._filepath, 0.5):
-            with atomic_writer(self._filepath, 'wb') as fp:
-                json.dump(data, fp, sort_keys=True, indent=2,
-                          encoding='utf-8')
-
-    # dict methods
-    def __setitem__(self, key, value):
-        """Implement :class:`dict` interface."""
-        if self._original.get(key) != value:
-            super(Settings, self).__setitem__(key, value)
-            self.save()
-
-    def __delitem__(self, key):
-        """Implement :class:`dict` interface."""
-        super(Settings, self).__delitem__(key)
-        self.save()
-
-    def update(self, *args, **kwargs):
-        """Override :class:`dict` method to save on update."""
-        super(Settings, self).update(*args, **kwargs)
-        self.save()
-
-    def setdefault(self, key, value=None):
-        """Override :class:`dict` method to save on update."""
-        ret = super(Settings, self).setdefault(key, value)
-        self.save()
-        return ret
-
-
-class Workflow(object):
-    """The ``Workflow`` object is the main interface to Alfred-Workflow.
-
-    It provides APIs for accessing the Alfred/workflow environment,
-    storing & caching data, using Keychain, and generating Script
-    Filter feedback.
-
-    ``Workflow`` is compatible with Alfred 2+. Subclass
-    :class:`~workflow.Workflow3` provides additional features,
-    only available in Alfred 3+, such as workflow variables.
-
-    :param default_settings: default workflow settings. If no settings file
-        exists, :class:`Workflow.settings` will be pre-populated with
-        ``default_settings``.
-    :type default_settings: :class:`dict`
-    :param update_settings: settings for updating your workflow from
-        GitHub releases. The only required key is ``github_slug``,
-        whose value must take the form of ``username/repo``.
-        If specified, ``Workflow`` will check the repo's releases
-        for updates. Your workflow must also have a semantic version
-        number. Please see the :ref:`User Manual <user-manual>` and
-        `update API docs <api-updates>` for more information.
-    :type update_settings: :class:`dict`
-    :param input_encoding: encoding of command line arguments. You
-        should probably leave this as the default (``utf-8``), which
-        is the encoding Alfred uses.
-    :type input_encoding: :class:`unicode`
-    :param normalization: normalisation to apply to CLI args.
-        See :meth:`Workflow.decode` for more details.
-    :type normalization: :class:`unicode`
-    :param capture_args: Capture and act on ``workflow:*`` arguments. See
-        :ref:`Magic arguments <magic-arguments>` for details.
-    :type capture_args: :class:`Boolean`
-    :param libraries: sequence of paths to directories containing
-        libraries. These paths will be prepended to ``sys.path``.
-    :type libraries: :class:`tuple` or :class:`list`
-    :param help_url: URL to webpage where a user can ask for help with
-        the workflow, report bugs, etc. This could be the GitHub repo
-        or a page on AlfredForum.com. If your workflow throws an error,
-        this URL will be displayed in the log and Alfred's debugger. It can
-        also be opened directly in a web browser with the ``workflow:help``
-        :ref:`magic argument <magic-arguments>`.
-    :type help_url: :class:`unicode` or :class:`str`
-
-    """
-
-    # Which class to use to generate feedback items. You probably
-    # won't want to change this
-    item_class = Item
-
-    def __init__(self, default_settings=None, update_settings=None,
-                 input_encoding='utf-8', normalization='NFC',
-                 capture_args=True, libraries=None,
-                 help_url=None):
-        """Create new :class:`Workflow` object."""
-        self._default_settings = default_settings or {}
-        self._update_settings = update_settings or {}
-        self._input_encoding = input_encoding
-        self._normalizsation = normalization
-        self._capture_args = capture_args
-        self.help_url = help_url
-        self._workflowdir = None
-        self._settings_path = None
-        self._settings = None
-        self._bundleid = None
-        self._debugging = None
-        self._name = None
-        self._cache_serializer = 'cpickle'
-        self._data_serializer = 'cpickle'
-        self._info = None
-        self._info_loaded = False
-        self._logger = None
-        self._items = []
-        self._alfred_env = None
-        # Version number of the workflow
-        self._version = UNSET
-        # Version from last workflow run
-        self._last_version_run = UNSET
-        # Cache for regex patterns created for filter keys
-        self._search_pattern_cache = {}
-        #: Prefix for all magic arguments.
-        #: The default value is ``workflow:`` so keyword
-        #: ``config`` would match user query ``workflow:config``.
-        self.magic_prefix = 'workflow:'
-        #: Mapping of available magic arguments. The built-in magic
-        #: arguments are registered by default. To add your own magic arguments
-        #: (or override built-ins), add a key:value pair where the key is
-        #: what the user should enter (prefixed with :attr:`magic_prefix`)
-        #: and the value is a callable that will be called when the argument
-        #: is entered. If you would like to display a message in Alfred, the
-        #: function should return a ``unicode`` string.
-        #:
-        #: By default, the magic arguments documented
-        #: :ref:`here <magic-arguments>` are registered.
-        self.magic_arguments = {}
-
-        self._register_default_magic()
-
-        if libraries:
-            sys.path = libraries + sys.path
-
-    ####################################################################
-    # API methods
-    ####################################################################
-
-    # info.plist contents and alfred_* environment variables  ----------
-
-    @property
-    def alfred_version(self):
-        """Alfred version as :class:`~workflow.update.Version` object."""
-        from update import Version
-        return Version(self.alfred_env.get('version'))
-
-    @property
-    def alfred_env(self):
-        """Dict of Alfred's environmental variables minus ``alfred_`` prefix.
-
-        .. versionadded:: 1.7
-
-        The variables Alfred 2.4+ exports are:
-
-        ============================  =========================================
-        Variable                      Description
-        ============================  =========================================
-        debug                         Set to ``1`` if Alfred's debugger is
-                                      open, otherwise unset.
-        preferences                   Path to Alfred.alfredpreferences
-                                      (where your workflows and settings are
-                                      stored).
-        preferences_localhash         Machine-specific preferences are stored
-                                      in ``Alfred.alfredpreferences/preferences/local/<hash>``
-                                      (see ``preferences`` above for
-                                      the path to ``Alfred.alfredpreferences``)
-        theme                         ID of selected theme
-        theme_background              Background colour of selected theme in
-                                      format ``rgba(r,g,b,a)``
-        theme_subtext                 Show result subtext.
-                                      ``0`` = Always,
-                                      ``1`` = Alternative actions only,
-                                      ``2`` = Selected result only,
-                                      ``3`` = Never
-        version                       Alfred version number, e.g. ``'2.4'``
-        version_build                 Alfred build number, e.g. ``277``
-        workflow_bundleid             Bundle ID, e.g.
-                                      ``net.deanishe.alfred-mailto``
-        workflow_cache                Path to workflow's cache directory
-        workflow_data                 Path to workflow's data directory
-        workflow_name                 Name of current workflow
-        workflow_uid                  UID of workflow
-        workflow_version              The version number specified in the
-                                      workflow configuration sheet/info.plist
-        ============================  =========================================
-
-        **Note:** all values are Unicode strings except ``version_build`` and
-        ``theme_subtext``, which are integers.
-
-        :returns: ``dict`` of Alfred's environmental variables without the
-            ``alfred_`` prefix, e.g. ``preferences``, ``workflow_data``.
-
-        """
-        if self._alfred_env is not None:
-            return self._alfred_env
-
-        data = {}
-
-        for key in (
-                'debug',
-                'preferences',
-                'preferences_localhash',
-                'theme',
-                'theme_background',
-                'theme_subtext',
-                'version',
-                'version_build',
-                'workflow_bundleid',
-                'workflow_cache',
-                'workflow_data',
-                'workflow_name',
-                'workflow_uid',
-                'workflow_version'):
-
-            value = os.getenv('alfred_' + key, '')
-
-            if value:
-                if key in ('debug', 'version_build', 'theme_subtext'):
-                    value = int(value)
-                else:
-                    value = self.decode(value)
-
-            data[key] = value
-
-        self._alfred_env = data
-
-        return self._alfred_env
-
-    @property
-    def info(self):
-        """:class:`dict` of ``info.plist`` contents."""
-        if not self._info_loaded:
-            self._load_info_plist()
-        return self._info
-
-    @property
-    def bundleid(self):
-        """Workflow bundle ID from environmental vars or ``info.plist``.
-
-        :returns: bundle ID
-        :rtype: ``unicode``
-
-        """
-        if not self._bundleid:
-            if self.alfred_env.get('workflow_bundleid'):
-                self._bundleid = self.alfred_env.get('workflow_bundleid')
-            else:
-                self._bundleid = unicode(self.info['bundleid'], 'utf-8')
-
-        return self._bundleid
-
-    @property
-    def debugging(self):
-        """Whether Alfred's debugger is open.
-
-        :returns: ``True`` if Alfred's debugger is open.
-        :rtype: ``bool``
-
-        """
-        return self.alfred_env.get('debug') == 1
-
-    @property
-    def name(self):
-        """Workflow name from Alfred's environmental vars or ``info.plist``.
-
-        :returns: workflow name
-        :rtype: ``unicode``
-
-        """
-        if not self._name:
-            if self.alfred_env.get('workflow_name'):
-                self._name = self.decode(self.alfred_env.get('workflow_name'))
-            else:
-                self._name = self.decode(self.info['name'])
-
-        return self._name
-
-    @property
-    def version(self):
-        """Return the version of the workflow.
-
-        .. versionadded:: 1.9.10
-
-        Get the workflow version from environment variable,
-        the ``update_settings`` dict passed on
-        instantiation, the ``version`` file located in the workflow's
-        root directory or ``info.plist``. Return ``None`` if none
-        exists or :class:`ValueError` if the version number is invalid
-        (i.e. not semantic).
-
-        :returns: Version of the workflow (not Alfred-Workflow)
-        :rtype: :class:`~workflow.update.Version` object
-
-        """
-        if self._version is UNSET:
-
-            version = None
-            # environment variable has priority
-            if self.alfred_env.get('workflow_version'):
-                version = self.alfred_env['workflow_version']
-
-            # Try `update_settings`
-            elif self._update_settings:
-                version = self._update_settings.get('version')
-
-            # `version` file
-            if not version:
-                filepath = self.workflowfile('version')
-
-                if os.path.exists(filepath):
-                    with open(filepath, 'rb') as fileobj:
-                        version = fileobj.read()
-
-            # info.plist
-            if not version:
-                version = self.info.get('version')
-
-            if version:
-                from update import Version
-                version = Version(version)
-
-            self._version = version
-
-        return self._version
-
-    # Workflow utility methods -----------------------------------------
-
-    @property
-    def args(self):
-        """Return command line args as normalised unicode.
-
-        Args are decoded and normalised via :meth:`~Workflow.decode`.
-
-        The encoding and normalisation are the ``input_encoding`` and
-        ``normalization`` arguments passed to :class:`Workflow` (``UTF-8``
-        and ``NFC`` are the defaults).
-
-        If :class:`Workflow` is called with ``capture_args=True``
-        (the default), :class:`Workflow` will look for certain
-        ``workflow:*`` args and, if found, perform the corresponding
-        actions and exit the workflow.
-
-        See :ref:`Magic arguments <magic-arguments>` for details.
-
-        """
-        msg = None
-        args = [self.decode(arg) for arg in sys.argv[1:]]
-
-        # Handle magic args
-        if len(args) and self._capture_args:
-            for name in self.magic_arguments:
-                key = '{0}{1}'.format(self.magic_prefix, name)
-                if key in args:
-                    msg = self.magic_arguments[name]()
-
-            if msg:
-                self.logger.debug(msg)
-                if not sys.stdout.isatty():  # Show message in Alfred
-                    self.add_item(msg, valid=False, icon=ICON_INFO)
-                    self.send_feedback()
-                sys.exit(0)
-        return args
-
-    @property
-    def cachedir(self):
-        """Path to workflow's cache directory.
-
-        The cache directory is a subdirectory of Alfred's own cache directory
-        in ``~/Library/Caches``. The full path is in Alfred 4+ is:
-
-        ``~/Library/Caches/com.runningwithcrayons.Alfred/Workflow Data/<bundle id>``
-
-        For earlier versions:
-
-        ``~/Library/Caches/com.runningwithcrayons.Alfred-X/Workflow Data/<bundle id>``
-
-        where ``Alfred-X`` may be ``Alfred-2`` or ``Alfred-3``.
-
-        Returns:
-            unicode: full path to workflow's cache directory
-
-        """
-        if self.alfred_env.get('workflow_cache'):
-            dirpath = self.alfred_env.get('workflow_cache')
-
-        else:
-            dirpath = self._default_cachedir
-
-        return self._create(dirpath)
-
-    @property
-    def _default_cachedir(self):
-        """Alfred 2's default cache directory."""
-        return os.path.join(
-            os.path.expanduser(
-                '~/Library/Caches/com.runningwithcrayons.Alfred-2/'
-                'Workflow Data/'),
-            self.bundleid)
-
-    @property
-    def datadir(self):
-        """Path to workflow's data directory.
-
-        The data directory is a subdirectory of Alfred's own data directory in
-        ``~/Library/Application Support``. The full path for Alfred 4+ is:
-
-        ``~/Library/Application Support/Alfred/Workflow Data/<bundle id>``
-
-        For earlier versions, the path is:
-
-        ``~/Library/Application Support/Alfred X/Workflow Data/<bundle id>``
-
-        where ``Alfred X` is ``Alfred 2`` or ``Alfred 3``.
-
-        Returns:
-            unicode: full path to workflow data directory
-
-        """
-        if self.alfred_env.get('workflow_data'):
-            dirpath = self.alfred_env.get('workflow_data')
-
-        else:
-            dirpath = self._default_datadir
-
-        return self._create(dirpath)
-
-    @property
-    def _default_datadir(self):
-        """Alfred 2's default data directory."""
-        return os.path.join(os.path.expanduser(
-            '~/Library/Application Support/Alfred 2/Workflow Data/'),
-            self.bundleid)
-
-    @property
-    def workflowdir(self):
-        """Path to workflow's root directory (where ``info.plist`` is).
-
-        Returns:
-            unicode: full path to workflow root directory
-
-        """
-        if not self._workflowdir:
-            # Try the working directory first, then the directory
-            # the library is in. CWD will be the workflow root if
-            # a workflow is being run in Alfred
-            candidates = [
-                os.path.abspath(os.getcwdu()),
-                os.path.dirname(os.path.abspath(os.path.dirname(__file__)))]
-
-            # climb the directory tree until we find `info.plist`
-            for dirpath in candidates:
-
-                # Ensure directory path is Unicode
-                dirpath = self.decode(dirpath)
-
-                while True:
-                    if os.path.exists(os.path.join(dirpath, 'info.plist')):
-                        self._workflowdir = dirpath
-                        break
-
-                    elif dirpath == '/':
-                        # no `info.plist` found
-                        break
-
-                    # Check the parent directory
-                    dirpath = os.path.dirname(dirpath)
-
-                # No need to check other candidates
-                if self._workflowdir:
-                    break
-
-            if not self._workflowdir:
-                raise IOError("'info.plist' not found in directory tree")
-
-        return self._workflowdir
-
-    def cachefile(self, filename):
-        """Path to ``filename`` in workflow's cache directory.
-
-        Return absolute path to ``filename`` within your workflow's
-        :attr:`cache directory <Workflow.cachedir>`.
-
-        :param filename: basename of file
-        :type filename: ``unicode``
-        :returns: full path to file within cache directory
-        :rtype: ``unicode``
-
-        """
-        return os.path.join(self.cachedir, filename)
-
-    def datafile(self, filename):
-        """Path to ``filename`` in workflow's data directory.
-
-        Return absolute path to ``filename`` within your workflow's
-        :attr:`data directory <Workflow.datadir>`.
-
-        :param filename: basename of file
-        :type filename: ``unicode``
-        :returns: full path to file within data directory
-        :rtype: ``unicode``
-
-        """
-        return os.path.join(self.datadir, filename)
-
-    def workflowfile(self, filename):
-        """Return full path to ``filename`` in workflow's root directory.
-
-        :param filename: basename of file
-        :type filename: ``unicode``
-        :returns: full path to file within data directory
-        :rtype: ``unicode``
-
-        """
-        return os.path.join(self.workflowdir, filename)
-
-    @property
-    def logfile(self):
-        """Path to logfile.
-
-        :returns: path to logfile within workflow's cache directory
-        :rtype: ``unicode``
-
-        """
-        return self.cachefile('%s.log' % self.bundleid)
-
-    @property
-    def logger(self):
-        """Logger that logs to both console and a log file.
-
-        If Alfred's debugger is open, log level will be ``DEBUG``,
-        else it will be ``INFO``.
-
-        Use :meth:`open_log` to open the log file in Console.
-
-        :returns: an initialised :class:`~logging.Logger`
-
-        """
-        if self._logger:
-            return self._logger
-
-        # Initialise new logger and optionally handlers
-        logger = logging.getLogger('')
-
-        # Only add one set of handlers
-        # Exclude from coverage, as pytest will have configured the
-        # root logger already
-        if not len(logger.handlers):  # pragma: no cover
-
-            fmt = logging.Formatter(
-                '%(asctime)s %(filename)s:%(lineno)s'
-                ' %(levelname)-8s %(message)s',
-                datefmt='%H:%M:%S')
-
-            logfile = logging.handlers.RotatingFileHandler(
-                self.logfile,
-                maxBytes=1024 * 1024,
-                backupCount=1)
-            logfile.setFormatter(fmt)
-            logger.addHandler(logfile)
-
-            console = logging.StreamHandler()
-            console.setFormatter(fmt)
-            logger.addHandler(console)
-
-        if self.debugging:
-            logger.setLevel(logging.DEBUG)
-        else:
-            logger.setLevel(logging.INFO)
-
-        self._logger = logger
-
-        return self._logger
-
-    @logger.setter
-    def logger(self, logger):
-        """Set a custom logger.
-
-        :param logger: The logger to use
-        :type logger: `~logging.Logger` instance
-
-        """
-        self._logger = logger
-
-    @property
-    def settings_path(self):
-        """Path to settings file within workflow's data directory.
-
-        :returns: path to ``settings.json`` file
-        :rtype: ``unicode``
-
-        """
-        if not self._settings_path:
-            self._settings_path = self.datafile('settings.json')
-        return self._settings_path
-
-    @property
-    def settings(self):
-        """Return a dictionary subclass that saves itself when changed.
-
-        See :ref:`guide-settings` in the :ref:`user-manual` for more
-        information on how to use :attr:`settings` and **important
-        limitations** on what it can do.
-
-        :returns: :class:`~workflow.workflow.Settings` instance
-            initialised from the data in JSON file at
-            :attr:`settings_path` or if that doesn't exist, with the
-            ``default_settings`` :class:`dict` passed to
-            :class:`Workflow` on instantiation.
-        :rtype: :class:`~workflow.workflow.Settings` instance
-
-        """
-        if not self._settings:
-            self.logger.debug('reading settings from %s', self.settings_path)
-            self._settings = Settings(self.settings_path,
-                                      self._default_settings)
-        return self._settings
-
-    @property
-    def cache_serializer(self):
-        """Name of default cache serializer.
-
-        .. versionadded:: 1.8
-
-        This serializer is used by :meth:`cache_data()` and
-        :meth:`cached_data()`
-
-        See :class:`SerializerManager` for details.
-
-        :returns: serializer name
-        :rtype: ``unicode``
-
-        """
-        return self._cache_serializer
-
-    @cache_serializer.setter
-    def cache_serializer(self, serializer_name):
-        """Set the default cache serialization format.
-
-        .. versionadded:: 1.8
-
-        This serializer is used by :meth:`cache_data()` and
-        :meth:`cached_data()`
-
-        The specified serializer must already by registered with the
-        :class:`SerializerManager` at `~workflow.workflow.manager`,
-        otherwise a :class:`ValueError` will be raised.
-
-        :param serializer_name: Name of default serializer to use.
-        :type serializer_name:
-
-        """
-        if manager.serializer(serializer_name) is None:
-            raise ValueError(
-                'Unknown serializer : `{0}`. Register your serializer '
-                'with `manager` first.'.format(serializer_name))
-
-        self.logger.debug('default cache serializer: %s', serializer_name)
-
-        self._cache_serializer = serializer_name
-
-    @property
-    def data_serializer(self):
-        """Name of default data serializer.
-
-        .. versionadded:: 1.8
-
-        This serializer is used by :meth:`store_data()` and
-        :meth:`stored_data()`
-
-        See :class:`SerializerManager` for details.
-
-        :returns: serializer name
-        :rtype: ``unicode``
-
-        """
-        return self._data_serializer
-
-    @data_serializer.setter
-    def data_serializer(self, serializer_name):
-        """Set the default cache serialization format.
-
-        .. versionadded:: 1.8
-
-        This serializer is used by :meth:`store_data()` and
-        :meth:`stored_data()`
-
-        The specified serializer must already by registered with the
-        :class:`SerializerManager` at `~workflow.workflow.manager`,
-        otherwise a :class:`ValueError` will be raised.
-
-        :param serializer_name: Name of serializer to use by default.
-
-        """
-        if manager.serializer(serializer_name) is None:
-            raise ValueError(
-                'Unknown serializer : `{0}`. Register your serializer '
-                'with `manager` first.'.format(serializer_name))
-
-        self.logger.debug('default data serializer: %s', serializer_name)
-
-        self._data_serializer = serializer_name
-
-    def stored_data(self, name):
-        """Retrieve data from data directory.
-
-        Returns ``None`` if there are no data stored under ``name``.
-
-        .. versionadded:: 1.8
-
-        :param name: name of datastore
-
-        """
-        metadata_path = self.datafile('.{0}.alfred-workflow'.format(name))
-
-        if not os.path.exists(metadata_path):
-            self.logger.debug('no data stored for `%s`', name)
-            return None
-
-        with open(metadata_path, 'rb') as file_obj:
-            serializer_name = file_obj.read().strip()
-
-        serializer = manager.serializer(serializer_name)
-
-        if serializer is None:
-            raise ValueError(
-                'Unknown serializer `{0}`. Register a corresponding '
-                'serializer with `manager.register()` '
-                'to load this data.'.format(serializer_name))
-
-        self.logger.debug('data `%s` stored as `%s`', name, serializer_name)
-
-        filename = '{0}.{1}'.format(name, serializer_name)
-        data_path = self.datafile(filename)
-
-        if not os.path.exists(data_path):
-            self.logger.debug('no data stored: %s', name)
-            if os.path.exists(metadata_path):
-                os.unlink(metadata_path)
-
-            return None
-
-        with open(data_path, 'rb') as file_obj:
-            data = serializer.load(file_obj)
-
-        self.logger.debug('stored data loaded: %s', data_path)
-
-        return data
-
-    def store_data(self, name, data, serializer=None):
-        """Save data to data directory.
-
-        .. versionadded:: 1.8
-
-        If ``data`` is ``None``, the datastore will be deleted.
-
-        Note that the datastore does NOT support mutliple threads.
-
-        :param name: name of datastore
-        :param data: object(s) to store. **Note:** some serializers
-            can only handled certain types of data.
-        :param serializer: name of serializer to use. If no serializer
-            is specified, the default will be used. See
-            :class:`SerializerManager` for more information.
-        :returns: data in datastore or ``None``
-
-        """
-        # Ensure deletion is not interrupted by SIGTERM
-        @uninterruptible
-        def delete_paths(paths):
-            """Clear one or more data stores"""
-            for path in paths:
-                if os.path.exists(path):
-                    os.unlink(path)
-                    self.logger.debug('deleted data file: %s', path)
-
-        serializer_name = serializer or self.data_serializer
-
-        # In order for `stored_data()` to be able to load data stored with
-        # an arbitrary serializer, yet still have meaningful file extensions,
-        # the format (i.e. extension) is saved to an accompanying file
-        metadata_path = self.datafile('.{0}.alfred-workflow'.format(name))
-        filename = '{0}.{1}'.format(name, serializer_name)
-        data_path = self.datafile(filename)
-
-        if data_path == self.settings_path:
-            raise ValueError(
-                'Cannot save data to' +
-                '`{0}` with format `{1}`. '.format(name, serializer_name) +
-                "This would overwrite Alfred-Workflow's settings file.")
-
-        serializer = manager.serializer(serializer_name)
-
-        if serializer is None:
-            raise ValueError(
-                'Invalid serializer `{0}`. Register your serializer with '
-                '`manager.register()` first.'.format(serializer_name))
-
-        if data is None:  # Delete cached data
-            delete_paths((metadata_path, data_path))
-            return
-
-        # Ensure write is not interrupted by SIGTERM
-        @uninterruptible
-        def _store():
-            # Save file extension
-            with atomic_writer(metadata_path, 'wb') as file_obj:
-                file_obj.write(serializer_name)
-
-            with atomic_writer(data_path, 'wb') as file_obj:
-                serializer.dump(data, file_obj)
-
-        _store()
-
-        self.logger.debug('saved data: %s', data_path)
-
-    def cached_data(self, name, data_func=None, max_age=60):
-        """Return cached data if younger than ``max_age`` seconds.
-
-        Retrieve data from cache or re-generate and re-cache data if
-        stale/non-existant. If ``max_age`` is 0, return cached data no
-        matter how old.
-
-        :param name: name of datastore
-        :param data_func: function to (re-)generate data.
-        :type data_func: ``callable``
-        :param max_age: maximum age of cached data in seconds
-        :type max_age: ``int``
-        :returns: cached data, return value of ``data_func`` or ``None``
-            if ``data_func`` is not set
-
-        """
-        serializer = manager.serializer(self.cache_serializer)
-
-        cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
-        age = self.cached_data_age(name)
-
-        if (age < max_age or max_age == 0) and os.path.exists(cache_path):
-
-            with open(cache_path, 'rb') as file_obj:
-                self.logger.debug('loading cached data: %s', cache_path)
-                return serializer.load(file_obj)
-
-        if not data_func:
-            return None
-
-        data = data_func()
-        self.cache_data(name, data)
-
-        return data
-
-    def cache_data(self, name, data):
-        """Save ``data`` to cache under ``name``.
-
-        If ``data`` is ``None``, the corresponding cache file will be
-        deleted.
-
-        :param name: name of datastore
-        :param data: data to store. This may be any object supported by
-                the cache serializer
-
-        """
-        serializer = manager.serializer(self.cache_serializer)
-
-        cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
-
-        if data is None:
-            if os.path.exists(cache_path):
-                os.unlink(cache_path)
-                self.logger.debug('deleted cache file: %s', cache_path)
-            return
-
-        with atomic_writer(cache_path, 'wb') as file_obj:
-            serializer.dump(data, file_obj)
-
-        self.logger.debug('cached data: %s', cache_path)
-
-    def cached_data_fresh(self, name, max_age):
-        """Whether cache `name` is less than `max_age` seconds old.
-
-        :param name: name of datastore
-        :param max_age: maximum age of data in seconds
-        :type max_age: ``int``
-        :returns: ``True`` if data is less than ``max_age`` old, else
-            ``False``
-
-        """
-        age = self.cached_data_age(name)
-
-        if not age:
-            return False
-
-        return age < max_age
-
-    def cached_data_age(self, name):
-        """Return age in seconds of cache `name` or 0 if cache doesn't exist.
-
-        :param name: name of datastore
-        :type name: ``unicode``
-        :returns: age of datastore in seconds
-        :rtype: ``int``
-
-        """
-        cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer))
-
-        if not os.path.exists(cache_path):
-            return 0
-
-        return time.time() - os.stat(cache_path).st_mtime
-
-    def filter(self, query, items, key=lambda x: x, ascending=False,
-               include_score=False, min_score=0, max_results=0,
-               match_on=MATCH_ALL, fold_diacritics=True):
-        """Fuzzy search filter. Returns list of ``items`` that match ``query``.
-
-        ``query`` is case-insensitive. Any item that does not contain the
-        entirety of ``query`` is rejected.
-
-        If ``query`` is an empty string or contains only whitespace,
-        all items will match.
-
-        :param query: query to test items against
-        :type query: ``unicode``
-        :param items: iterable of items to test
-        :type items: ``list`` or ``tuple``
-        :param key: function to get comparison key from ``items``.
-            Must return a ``unicode`` string. The default simply returns
-            the item.
-        :type key: ``callable``
-        :param ascending: set to ``True`` to get worst matches first
-        :type ascending: ``Boolean``
-        :param include_score: Useful for debugging the scoring algorithm.
-            If ``True``, results will be a list of tuples
-            ``(item, score, rule)``.
-        :type include_score: ``Boolean``
-        :param min_score: If non-zero, ignore results with a score lower
-            than this.
-        :type min_score: ``int``
-        :param max_results: If non-zero, prune results list to this length.
-        :type max_results: ``int``
-        :param match_on: Filter option flags. Bitwise-combined list of
-            ``MATCH_*`` constants (see below).
-        :type match_on: ``int``
-        :param fold_diacritics: Convert search keys to ASCII-only
-            characters if ``query`` only contains ASCII characters.
-        :type fold_diacritics: ``Boolean``
-        :returns: list of ``items`` matching ``query`` or list of
-            ``(item, score, rule)`` `tuples` if ``include_score`` is ``True``.
-            ``rule`` is the ``MATCH_*`` rule that matched the item.
-        :rtype: ``list``
-
-        **Matching rules**
-
-        By default, :meth:`filter` uses all of the following flags (i.e.
-        :const:`MATCH_ALL`). The tests are always run in the given order:
-
-        1. :const:`MATCH_STARTSWITH`
-            Item search key starts with ``query`` (case-insensitive).
-        2. :const:`MATCH_CAPITALS`
-            The list of capital letters in item search key starts with
-            ``query`` (``query`` may be lower-case). E.g., ``of``
-            would match ``OmniFocus``, ``gc`` would match ``Google Chrome``.
-        3. :const:`MATCH_ATOM`
-            Search key is split into "atoms" on non-word characters
-            (.,-,' etc.). Matches if ``query`` is one of these atoms
-            (case-insensitive).
-        4. :const:`MATCH_INITIALS_STARTSWITH`
-            Initials are the first characters of the above-described
-            "atoms" (case-insensitive).
-        5. :const:`MATCH_INITIALS_CONTAIN`
-            ``query`` is a substring of the above-described initials.
-        6. :const:`MATCH_INITIALS`
-            Combination of (4) and (5).
-        7. :const:`MATCH_SUBSTRING`
-            ``query`` is a substring of item search key (case-insensitive).
-        8. :const:`MATCH_ALLCHARS`
-            All characters in ``query`` appear in item search key in
-            the same order (case-insensitive).
-        9. :const:`MATCH_ALL`
-            Combination of all the above.
-
-
-        :const:`MATCH_ALLCHARS` is considerably slower than the other
-        tests and provides much less accurate results.
-
-        **Examples:**
-
-        To ignore :const:`MATCH_ALLCHARS` (tends to provide the worst
-        matches and is expensive to run), use
-        ``match_on=MATCH_ALL ^ MATCH_ALLCHARS``.
-
-        To match only on capitals, use ``match_on=MATCH_CAPITALS``.
-
-        To match only on startswith and substring, use
-        ``match_on=MATCH_STARTSWITH | MATCH_SUBSTRING``.
-
-        **Diacritic folding**
-
-        .. versionadded:: 1.3
-
-        If ``fold_diacritics`` is ``True`` (the default), and ``query``
-        contains only ASCII characters, non-ASCII characters in search keys
-        will be converted to ASCII equivalents (e.g. **ü** -> **u**,
-        **ß** -> **ss**, **é** -> **e**).
-
-        See :const:`ASCII_REPLACEMENTS` for all replacements.
-
-        If ``query`` contains non-ASCII characters, search keys will not be
-        altered.
-
-        """
-        if not query:
-            return items
-
-        # Remove preceding/trailing spaces
-        query = query.strip()
-
-        if not query:
-            return items
-
-        # Use user override if there is one
-        fold_diacritics = self.settings.get('__workflow_diacritic_folding',
-                                            fold_diacritics)
-
-        results = []
-
-        for item in items:
-            skip = False
-            score = 0
-            words = [s.strip() for s in query.split(' ')]
-            value = key(item).strip()
-            if value == '':
-                continue
-            for word in words:
-                if word == '':
-                    continue
-                s, rule = self._filter_item(value, word, match_on,
-                                            fold_diacritics)
-
-                if not s:  # Skip items that don't match part of the query
-                    skip = True
-                score += s
-
-            if skip:
-                continue
-
-            if score:
-                # use "reversed" `score` (i.e. highest becomes lowest) and
-                # `value` as sort key. This means items with the same score
-                # will be sorted in alphabetical not reverse alphabetical order
-                results.append(((100.0 / score, value.lower(), score),
-                                (item, score, rule)))
-
-        # sort on keys, then discard the keys
-        results.sort(reverse=ascending)
-        results = [t[1] for t in results]
-
-        if min_score:
-            results = [r for r in results if r[1] > min_score]
-
-        if max_results and len(results) > max_results:
-            results = results[:max_results]
-
-        # return list of ``(item, score, rule)``
-        if include_score:
-            return results
-        # just return list of items
-        return [t[0] for t in results]
-
-    def _filter_item(self, value, query, match_on, fold_diacritics):
-        """Filter ``value`` against ``query`` using rules ``match_on``.
-
-        :returns: ``(score, rule)``
-
-        """
-        query = query.lower()
-
-        if not isascii(query):
-            fold_diacritics = False
-
-        if fold_diacritics:
-            value = self.fold_to_ascii(value)
-
-        # pre-filter any items that do not contain all characters
-        # of ``query`` to save on running several more expensive tests
-        if not set(query) <= set(value.lower()):
-
-            return (0, None)
-
-        # item starts with query
-        if match_on & MATCH_STARTSWITH and value.lower().startswith(query):
-            score = 100.0 - (len(value) / len(query))
-
-            return (score, MATCH_STARTSWITH)
-
-        # query matches capitalised letters in item,
-        # e.g. of = OmniFocus
-        if match_on & MATCH_CAPITALS:
-            initials = ''.join([c for c in value if c in INITIALS])
-            if initials.lower().startswith(query):
-                score = 100.0 - (len(initials) / len(query))
-
-                return (score, MATCH_CAPITALS)
-
-        # split the item into "atoms", i.e. words separated by
-        # spaces or other non-word characters
-        if (match_on & MATCH_ATOM or
-                match_on & MATCH_INITIALS_CONTAIN or
-                match_on & MATCH_INITIALS_STARTSWITH):
-            atoms = [s.lower() for s in split_on_delimiters(value)]
-            # print('atoms : %s  -->  %s' % (value, atoms))
-            # initials of the atoms
-            initials = ''.join([s[0] for s in atoms if s])
-
-        if match_on & MATCH_ATOM:
-            # is `query` one of the atoms in item?
-            # similar to substring, but scores more highly, as it's
-            # a word within the item
-            if query in atoms:
-                score = 100.0 - (len(value) / len(query))
-
-                return (score, MATCH_ATOM)
-
-        # `query` matches start (or all) of the initials of the
-        # atoms, e.g. ``himym`` matches "How I Met Your Mother"
-        # *and* "how i met your mother" (the ``capitals`` rule only
-        # matches the former)
-        if (match_on & MATCH_INITIALS_STARTSWITH and
-                initials.startswith(query)):
-            score = 100.0 - (len(initials) / len(query))
-
-            return (score, MATCH_INITIALS_STARTSWITH)
-
-        # `query` is a substring of initials, e.g. ``doh`` matches
-        # "The Dukes of Hazzard"
-        elif (match_on & MATCH_INITIALS_CONTAIN and
-                query in initials):
-            score = 95.0 - (len(initials) / len(query))
-
-            return (score, MATCH_INITIALS_CONTAIN)
-
-        # `query` is a substring of item
-        if match_on & MATCH_SUBSTRING and query in value.lower():
-            score = 90.0 - (len(value) / len(query))
-
-            return (score, MATCH_SUBSTRING)
-
-        # finally, assign a score based on how close together the
-        # characters in `query` are in item.
-        if match_on & MATCH_ALLCHARS:
-            search = self._search_for_query(query)
-            match = search(value)
-            if match:
-                score = 100.0 / ((1 + match.start()) *
-                                 (match.end() - match.start() + 1))
-
-                return (score, MATCH_ALLCHARS)
-
-        # Nothing matched
-        return (0, None)
-
-    def _search_for_query(self, query):
-        if query in self._search_pattern_cache:
-            return self._search_pattern_cache[query]
-
-        # Build pattern: include all characters
-        pattern = []
-        for c in query:
-            # pattern.append('[^{0}]*{0}'.format(re.escape(c)))
-            pattern.append('.*?{0}'.format(re.escape(c)))
-        pattern = ''.join(pattern)
-        search = re.compile(pattern, re.IGNORECASE).search
-
-        self._search_pattern_cache[query] = search
-        return search
-
-    def run(self, func, text_errors=False):
-        """Call ``func`` to run your workflow.
-
-        :param func: Callable to call with ``self`` (i.e. the :class:`Workflow`
-            instance) as first argument.
-        :param text_errors: Emit error messages in plain text, not in
-            Alfred's XML/JSON feedback format. Use this when you're not
-            running Alfred-Workflow in a Script Filter and would like
-            to pass the error message to, say, a notification.
-        :type text_errors: ``Boolean``
-
-        ``func`` will be called with :class:`Workflow` instance as first
-        argument.
-
-        ``func`` should be the main entry point to your workflow.
-
-        Any exceptions raised will be logged and an error message will be
-        output to Alfred.
-
-        """
-        start = time.time()
-
-        # Write to debugger to ensure "real" output starts on a new line
-        print('.', file=sys.stderr)
-
-        # Call workflow's entry function/method within a try-except block
-        # to catch any errors and display an error message in Alfred
-        try:
-            if self.version:
-                self.logger.debug('---------- %s (%s) ----------',
-                                  self.name, self.version)
-            else:
-                self.logger.debug('---------- %s ----------', self.name)
-
-            # Run update check if configured for self-updates.
-            # This call has to go in the `run` try-except block, as it will
-            # initialise `self.settings`, which will raise an exception
-            # if `settings.json` isn't valid.
-            if self._update_settings:
-                self.check_update()
-
-            # Run workflow's entry function/method
-            func(self)
-
-            # Set last version run to current version after a successful
-            # run
-            self.set_last_version()
-
-        except Exception as err:
-            self.logger.exception(err)
-            if self.help_url:
-                self.logger.info('for assistance, see: %s', self.help_url)
-
-            if not sys.stdout.isatty():  # Show error in Alfred
-                if text_errors:
-                    print(unicode(err).encode('utf-8'), end='')
-                else:
-                    self._items = []
-                    if self._name:
-                        name = self._name
-                    elif self._bundleid:  # pragma: no cover
-                        name = self._bundleid
-                    else:  # pragma: no cover
-                        name = os.path.dirname(__file__)
-                    self.add_item("Error in workflow '%s'" % name,
-                                  unicode(err),
-                                  icon=ICON_ERROR)
-                    self.send_feedback()
-            return 1
-
-        finally:
-            self.logger.debug('---------- finished in %0.3fs ----------',
-                              time.time() - start)
-
-        return 0
-
-    # Alfred feedback methods ------------------------------------------
-
-    def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None,
-                 autocomplete=None, valid=False, uid=None, icon=None,
-                 icontype=None, type=None, largetext=None, copytext=None,
-                 quicklookurl=None):
-        """Add an item to be output to Alfred.
-
-        :param title: Title shown in Alfred
-        :type title: ``unicode``
-        :param subtitle: Subtitle shown in Alfred
-        :type subtitle: ``unicode``
-        :param modifier_subtitles: Subtitles shown when modifier
-            (CMD, OPT etc.) is pressed. Use a ``dict`` with the lowercase
-            keys ``cmd``, ``ctrl``, ``shift``, ``alt`` and ``fn``
-        :type modifier_subtitles: ``dict``
-        :param arg: Argument passed by Alfred as ``{query}`` when item is
-            actioned
-        :type arg: ``unicode``
-        :param autocomplete: Text expanded in Alfred when item is TABbed
-        :type autocomplete: ``unicode``
-        :param valid: Whether or not item can be actioned
-        :type valid: ``Boolean``
-        :param uid: Used by Alfred to remember/sort items
-        :type uid: ``unicode``
-        :param icon: Filename of icon to use
-        :type icon: ``unicode``
-        :param icontype: Type of icon. Must be one of ``None`` , ``'filetype'``
-           or ``'fileicon'``. Use ``'filetype'`` when ``icon`` is a filetype
-           such as ``'public.folder'``. Use ``'fileicon'`` when you wish to
-           use the icon of the file specified as ``icon``, e.g.
-           ``icon='/Applications/Safari.app', icontype='fileicon'``.
-           Leave as `None` if ``icon`` points to an actual
-           icon file.
-        :type icontype: ``unicode``
-        :param type: Result type. Currently only ``'file'`` is supported
-            (by Alfred). This will tell Alfred to enable file actions for
-            this item.
-        :type type: ``unicode``
-        :param largetext: Text to be displayed in Alfred's large text box
-            if user presses CMD+L on item.
-        :type largetext: ``unicode``
-        :param copytext: Text to be copied to pasteboard if user presses
-            CMD+C on item.
-        :type copytext: ``unicode``
-        :param quicklookurl: URL to be displayed using Alfred's Quick Look
-            feature (tapping ``SHIFT`` or ``⌘+Y`` on a result).
-        :type quicklookurl: ``unicode``
-        :returns: :class:`Item` instance
-
-        See :ref:`icons` for a list of the supported system icons.
-
-        .. note::
-
-            Although this method returns an :class:`Item` instance, you don't
-            need to hold onto it or worry about it. All generated :class:`Item`
-            instances are also collected internally and sent to Alfred when
-            :meth:`send_feedback` is called.
-
-            The generated :class:`Item` is only returned in case you want to
-            edit it or do something with it other than send it to Alfred.
-
-        """
-        item = self.item_class(title, subtitle, modifier_subtitles, arg,
-                               autocomplete, valid, uid, icon, icontype, type,
-                               largetext, copytext, quicklookurl)
-        self._items.append(item)
-        return item
-
-    def send_feedback(self):
-        """Print stored items to console/Alfred as XML."""
-        root = ET.Element('items')
-        for item in self._items:
-            root.append(item.elem)
-        sys.stdout.write('<?xml version="1.0" encoding="utf-8"?>\n')
-        sys.stdout.write(ET.tostring(root).encode('utf-8'))
-        sys.stdout.flush()
-
-    ####################################################################
-    # Updating methods
-    ####################################################################
-
-    @property
-    def first_run(self):
-        """Return ``True`` if it's the first time this version has run.
-
-        .. versionadded:: 1.9.10
-
-        Raises a :class:`ValueError` if :attr:`version` isn't set.
-
-        """
-        if not self.version:
-            raise ValueError('No workflow version set')
-
-        if not self.last_version_run:
-            return True
-
-        return self.version != self.last_version_run
-
-    @property
-    def last_version_run(self):
-        """Return version of last version to run (or ``None``).
-
-        .. versionadded:: 1.9.10
-
-        :returns: :class:`~workflow.update.Version` instance
-            or ``None``
-
-        """
-        if self._last_version_run is UNSET:
-
-            version = self.settings.get('__workflow_last_version')
-            if version:
-                from update import Version
-                version = Version(version)
-
-            self._last_version_run = version
-
-        self.logger.debug('last run version: %s', self._last_version_run)
-
-        return self._last_version_run
-
-    def set_last_version(self, version=None):
-        """Set :attr:`last_version_run` to current version.
-
-        .. versionadded:: 1.9.10
-
-        :param version: version to store (default is current version)
-        :type version: :class:`~workflow.update.Version` instance
-            or ``unicode``
-        :returns: ``True`` if version is saved, else ``False``
-
-        """
-        if not version:
-            if not self.version:
-                self.logger.warning(
-                    "Can't save last version: workflow has no version")
-                return False
-
-            version = self.version
-
-        if isinstance(version, basestring):
-            from update import Version
-            version = Version(version)
-
-        self.settings['__workflow_last_version'] = str(version)
-
-        self.logger.debug('set last run version: %s', version)
-
-        return True
-
-    @property
-    def update_available(self):
-        """Whether an update is available.
-
-        .. versionadded:: 1.9
-
-        See :ref:`guide-updates` in the :ref:`user-manual` for detailed
-        information on how to enable your workflow to update itself.
-
-        :returns: ``True`` if an update is available, else ``False``
-
-        """
-        key = '__workflow_latest_version'
-        # Create a new workflow object to ensure standard serialiser
-        # is used (update.py is called without the user's settings)
-        status = Workflow().cached_data(key, max_age=0)
-
-        # self.logger.debug('update status: %r', status)
-        if not status or not status.get('available'):
-            return False
-
-        return status['available']
-
-    @property
-    def prereleases(self):
-        """Whether workflow should update to pre-release versions.
-
-        .. versionadded:: 1.16
-
-        :returns: ``True`` if pre-releases are enabled with the :ref:`magic
-            argument <magic-arguments>` or the ``update_settings`` dict, else
-            ``False``.
-
-        """
-        if self._update_settings.get('prereleases'):
-            return True
-
-        return self.settings.get('__workflow_prereleases') or False
-
-    def check_update(self, force=False):
-        """Call update script if it's time to check for a new release.
-
-        .. versionadded:: 1.9
-
-        The update script will be run in the background, so it won't
-        interfere in the execution of your workflow.
-
-        See :ref:`guide-updates` in the :ref:`user-manual` for detailed
-        information on how to enable your workflow to update itself.
-
-        :param force: Force update check
-        :type force: ``Boolean``
-
-        """
-        key = '__workflow_latest_version'
-        frequency = self._update_settings.get('frequency',
-                                              DEFAULT_UPDATE_FREQUENCY)
-
-        if not force and not self.settings.get('__workflow_autoupdate', True):
-            self.logger.debug('Auto update turned off by user')
-            return
-
-        # Check for new version if it's time
-        if (force or not self.cached_data_fresh(key, frequency * 86400)):
-
-            repo = self._update_settings['github_slug']
-            # version = self._update_settings['version']
-            version = str(self.version)
-
-            from background import run_in_background
-
-            # update.py is adjacent to this file
-            update_script = os.path.join(os.path.dirname(__file__),
-                                         b'update.py')
-
-            cmd = ['/usr/bin/python', update_script, 'check', repo, version]
-
-            if self.prereleases:
-                cmd.append('--prereleases')
-
-            self.logger.info('checking for update ...')
-
-            run_in_background('__workflow_update_check', cmd)
-
-        else:
-            self.logger.debug('update check not due')
-
-    def start_update(self):
-        """Check for update and download and install new workflow file.
-
-        .. versionadded:: 1.9
-
-        See :ref:`guide-updates` in the :ref:`user-manual` for detailed
-        information on how to enable your workflow to update itself.
-
-        :returns: ``True`` if an update is available and will be
-            installed, else ``False``
-
-        """
-        import update
-
-        repo = self._update_settings['github_slug']
-        # version = self._update_settings['version']
-        version = str(self.version)
-
-        if not update.check_update(repo, version, self.prereleases):
-            return False
-
-        from background import run_in_background
-
-        # update.py is adjacent to this file
-        update_script = os.path.join(os.path.dirname(__file__),
-                                     b'update.py')
-
-        cmd = ['/usr/bin/python', update_script, 'install', repo, version]
-
-        if self.prereleases:
-            cmd.append('--prereleases')
-
-        self.logger.debug('downloading update ...')
-        run_in_background('__workflow_update_install', cmd)
-
-        return True
-
-    ####################################################################
-    # Keychain password storage methods
-    ####################################################################
-
-    def save_password(self, account, password, service=None):
-        """Save account credentials.
-
-        If the account exists, the old password will first be deleted
-        (Keychain throws an error otherwise).
-
-        If something goes wrong, a :class:`KeychainError` exception will
-        be raised.
-
-        :param account: name of the account the password is for, e.g.
-            "Pinboard"
-        :type account: ``unicode``
-        :param password: the password to secure
-        :type password: ``unicode``
-        :param service: Name of the service. By default, this is the
-            workflow's bundle ID
-        :type service: ``unicode``
-
-        """
-        if not service:
-            service = self.bundleid
-
-        try:
-            self._call_security('add-generic-password', service, account,
-                                '-w', password)
-            self.logger.debug('saved password : %s:%s', service, account)
-
-        except PasswordExists:
-            self.logger.debug('password exists : %s:%s', service, account)
-            current_password = self.get_password(account, service)
-
-            if current_password == password:
-                self.logger.debug('password unchanged')
-
-            else:
-                self.delete_password(account, service)
-                self._call_security('add-generic-password', service,
-                                    account, '-w', password)
-                self.logger.debug('save_password : %s:%s', service, account)
-
-    def get_password(self, account, service=None):
-        """Retrieve the password saved at ``service/account``.
-
-        Raise :class:`PasswordNotFound` exception if password doesn't exist.
-
-        :param account: name of the account the password is for, e.g.
-            "Pinboard"
-        :type account: ``unicode``
-        :param service: Name of the service. By default, this is the workflow's
-                        bundle ID
-        :type service: ``unicode``
-        :returns: account password
-        :rtype: ``unicode``
-
-        """
-        if not service:
-            service = self.bundleid
-
-        output = self._call_security('find-generic-password', service,
-                                     account, '-g')
-
-        # Parsing of `security` output is adapted from python-keyring
-        # by Jason R. Coombs
-        # https://pypi.python.org/pypi/keyring
-        m = re.search(
-            r'password:\s*(?:0x(?P<hex>[0-9A-F]+)\s*)?(?:"(?P<pw>.*)")?',
-            output)
-
-        if m:
-            groups = m.groupdict()
-            h = groups.get('hex')
-            password = groups.get('pw')
-            if h:
-                password = unicode(binascii.unhexlify(h), 'utf-8')
-
-        self.logger.debug('got password : %s:%s', service, account)
-
-        return password
-
-    def delete_password(self, account, service=None):
-        """Delete the password stored at ``service/account``.
-
-        Raise :class:`PasswordNotFound` if account is unknown.
-
-        :param account: name of the account the password is for, e.g.
-            "Pinboard"
-        :type account: ``unicode``
-        :param service: Name of the service. By default, this is the workflow's
-                        bundle ID
-        :type service: ``unicode``
-
-        """
-        if not service:
-            service = self.bundleid
-
-        self._call_security('delete-generic-password', service, account)
-
-        self.logger.debug('deleted password : %s:%s', service, account)
-
-    ####################################################################
-    # Methods for workflow:* magic args
-    ####################################################################
-
-    def _register_default_magic(self):
-        """Register the built-in magic arguments."""
-        # TODO: refactor & simplify
-        # Wrap callback and message with callable
-        def callback(func, msg):
-            def wrapper():
-                func()
-                return msg
-
-            return wrapper
-
-        self.magic_arguments['delcache'] = callback(self.clear_cache,
-                                                    'Deleted workflow cache')
-        self.magic_arguments['deldata'] = callback(self.clear_data,
-                                                   'Deleted workflow data')
-        self.magic_arguments['delsettings'] = callback(
-            self.clear_settings, 'Deleted workflow settings')
-        self.magic_arguments['reset'] = callback(self.reset,
-                                                 'Reset workflow')
-        self.magic_arguments['openlog'] = callback(self.open_log,
-                                                   'Opening workflow log file')
-        self.magic_arguments['opencache'] = callback(
-            self.open_cachedir, 'Opening workflow cache directory')
-        self.magic_arguments['opendata'] = callback(
-            self.open_datadir, 'Opening workflow data directory')
-        self.magic_arguments['openworkflow'] = callback(
-            self.open_workflowdir, 'Opening workflow directory')
-        self.magic_arguments['openterm'] = callback(
-            self.open_terminal, 'Opening workflow root directory in Terminal')
-
-        # Diacritic folding
-        def fold_on():
-            self.settings['__workflow_diacritic_folding'] = True
-            return 'Diacritics will always be folded'
-
-        def fold_off():
-            self.settings['__workflow_diacritic_folding'] = False
-            return 'Diacritics will never be folded'
-
-        def fold_default():
-            if '__workflow_diacritic_folding' in self.settings:
-                del self.settings['__workflow_diacritic_folding']
-            return 'Diacritics folding reset'
-
-        self.magic_arguments['foldingon'] = fold_on
-        self.magic_arguments['foldingoff'] = fold_off
-        self.magic_arguments['foldingdefault'] = fold_default
-
-        # Updates
-        def update_on():
-            self.settings['__workflow_autoupdate'] = True
-            return 'Auto update turned on'
-
-        def update_off():
-            self.settings['__workflow_autoupdate'] = False
-            return 'Auto update turned off'
-
-        def prereleases_on():
-            self.settings['__workflow_prereleases'] = True
-            return 'Prerelease updates turned on'
-
-        def prereleases_off():
-            self.settings['__workflow_prereleases'] = False
-            return 'Prerelease updates turned off'
-
-        def do_update():
-            if self.start_update():
-                return 'Downloading and installing update ...'
-            else:
-                return 'No update available'
-
-        self.magic_arguments['autoupdate'] = update_on
-        self.magic_arguments['noautoupdate'] = update_off
-        self.magic_arguments['prereleases'] = prereleases_on
-        self.magic_arguments['noprereleases'] = prereleases_off
-        self.magic_arguments['update'] = do_update
-
-        # Help
-        def do_help():
-            if self.help_url:
-                self.open_help()
-                return 'Opening workflow help URL in browser'
-            else:
-                return 'Workflow has no help URL'
-
-        def show_version():
-            if self.version:
-                return 'Version: {0}'.format(self.version)
-            else:
-                return 'This workflow has no version number'
-
-        def list_magic():
-            """Display all available magic args in Alfred."""
-            isatty = sys.stderr.isatty()
-            for name in sorted(self.magic_arguments.keys()):
-                if name == 'magic':
-                    continue
-                arg = self.magic_prefix + name
-                self.logger.debug(arg)
-
-                if not isatty:
-                    self.add_item(arg, icon=ICON_INFO)
-
-            if not isatty:
-                self.send_feedback()
-
-        self.magic_arguments['help'] = do_help
-        self.magic_arguments['magic'] = list_magic
-        self.magic_arguments['version'] = show_version
-
-    def clear_cache(self, filter_func=lambda f: True):
-        """Delete all files in workflow's :attr:`cachedir`.
-
-        :param filter_func: Callable to determine whether a file should be
-            deleted or not. ``filter_func`` is called with the filename
-            of each file in the data directory. If it returns ``True``,
-            the file will be deleted.
-            By default, *all* files will be deleted.
-        :type filter_func: ``callable``
-        """
-        self._delete_directory_contents(self.cachedir, filter_func)
-
-    def clear_data(self, filter_func=lambda f: True):
-        """Delete all files in workflow's :attr:`datadir`.
-
-        :param filter_func: Callable to determine whether a file should be
-            deleted or not. ``filter_func`` is called with the filename
-            of each file in the data directory. If it returns ``True``,
-            the file will be deleted.
-            By default, *all* files will be deleted.
-        :type filter_func: ``callable``
-        """
-        self._delete_directory_contents(self.datadir, filter_func)
-
-    def clear_settings(self):
-        """Delete workflow's :attr:`settings_path`."""
-        if os.path.exists(self.settings_path):
-            os.unlink(self.settings_path)
-            self.logger.debug('deleted : %r', self.settings_path)
-
-    def reset(self):
-        """Delete workflow settings, cache and data.
-
-        File :attr:`settings <settings_path>` and directories
-        :attr:`cache <cachedir>` and :attr:`data <datadir>` are deleted.
-
-        """
-        self.clear_cache()
-        self.clear_data()
-        self.clear_settings()
-
-    def open_log(self):
-        """Open :attr:`logfile` in default app (usually Console.app)."""
-        subprocess.call(['open', self.logfile])  # nosec
-
-    def open_cachedir(self):
-        """Open the workflow's :attr:`cachedir` in Finder."""
-        subprocess.call(['open', self.cachedir])  # nosec
-
-    def open_datadir(self):
-        """Open the workflow's :attr:`datadir` in Finder."""
-        subprocess.call(['open', self.datadir])  # nosec
-
-    def open_workflowdir(self):
-        """Open the workflow's :attr:`workflowdir` in Finder."""
-        subprocess.call(['open', self.workflowdir])  # nosec
-
-    def open_terminal(self):
-        """Open a Terminal window at workflow's :attr:`workflowdir`."""
-        subprocess.call(['open', '-a', 'Terminal', self.workflowdir])  # nosec
-
-    def open_help(self):
-        """Open :attr:`help_url` in default browser."""
-        subprocess.call(['open', self.help_url])  # nosec
-
-        return 'Opening workflow help URL in browser'
-
-    ####################################################################
-    # Helper methods
-    ####################################################################
-
-    def decode(self, text, encoding=None, normalization=None):
-        """Return ``text`` as normalised unicode.
-
-        If ``encoding`` and/or ``normalization`` is ``None``, the
-        ``input_encoding``and ``normalization`` parameters passed to
-        :class:`Workflow` are used.
-
-        :param text: string
-        :type text: encoded or Unicode string. If ``text`` is already a
-            Unicode string, it will only be normalised.
-        :param encoding: The text encoding to use to decode ``text`` to
-            Unicode.
-        :type encoding: ``unicode`` or ``None``
-        :param normalization: The nomalisation form to apply to ``text``.
-        :type normalization: ``unicode`` or ``None``
-        :returns: decoded and normalised ``unicode``
-
-        :class:`Workflow` uses "NFC" normalisation by default. This is the
-        standard for Python and will work well with data from the web (via
-        :mod:`~workflow.web` or :mod:`json`).
-
-        macOS, on the other hand, uses "NFD" normalisation (nearly), so data
-        coming from the system (e.g. via :mod:`subprocess` or
-        :func:`os.listdir`/:mod:`os.path`) may not match. You should either
-        normalise this data, too, or change the default normalisation used by
-        :class:`Workflow`.
-
-        """
-        encoding = encoding or self._input_encoding
-        normalization = normalization or self._normalizsation
-        if not isinstance(text, unicode):
-            text = unicode(text, encoding)
-        return unicodedata.normalize(normalization, text)
-
-    def fold_to_ascii(self, text):
-        """Convert non-ASCII characters to closest ASCII equivalent.
-
-        .. versionadded:: 1.3
-
-        .. note:: This only works for a subset of European languages.
-
-        :param text: text to convert
-        :type text: ``unicode``
-        :returns: text containing only ASCII characters
-        :rtype: ``unicode``
-
-        """
-        if isascii(text):
-            return text
-        text = ''.join([ASCII_REPLACEMENTS.get(c, c) for c in text])
-        return unicode(unicodedata.normalize('NFKD',
-                       text).encode('ascii', 'ignore'))
-
-    def dumbify_punctuation(self, text):
-        """Convert non-ASCII punctuation to closest ASCII equivalent.
-
-        This method replaces "smart" quotes and n- or m-dashes with their
-        workaday ASCII equivalents. This method is currently not used
-        internally, but exists as a helper method for workflow authors.
-
-        .. versionadded: 1.9.7
-
-        :param text: text to convert
-        :type text: ``unicode``
-        :returns: text with only ASCII punctuation
-        :rtype: ``unicode``
-
-        """
-        if isascii(text):
-            return text
-
-        text = ''.join([DUMB_PUNCTUATION.get(c, c) for c in text])
-        return text
-
-    def _delete_directory_contents(self, dirpath, filter_func):
-        """Delete all files in a directory.
-
-        :param dirpath: path to directory to clear
-        :type dirpath: ``unicode`` or ``str``
-        :param filter_func function to determine whether a file shall be
-            deleted or not.
-        :type filter_func ``callable``
-
-        """
-        if os.path.exists(dirpath):
-            for filename in os.listdir(dirpath):
-                if not filter_func(filename):
-                    continue
-                path = os.path.join(dirpath, filename)
-                if os.path.isdir(path):
-                    shutil.rmtree(path)
-                else:
-                    os.unlink(path)
-                self.logger.debug('deleted : %r', path)
-
-    def _load_info_plist(self):
-        """Load workflow info from ``info.plist``."""
-        # info.plist should be in the directory above this one
-        self._info = plistlib.readPlist(self.workflowfile('info.plist'))
-        self._info_loaded = True
-
-    def _create(self, dirpath):
-        """Create directory `dirpath` if it doesn't exist.
-
-        :param dirpath: path to directory
-        :type dirpath: ``unicode``
-        :returns: ``dirpath`` argument
-        :rtype: ``unicode``
-
-        """
-        if not os.path.exists(dirpath):
-            os.makedirs(dirpath)
-        return dirpath
-
-    def _call_security(self, action, service, account, *args):
-        """Call ``security`` CLI program that provides access to keychains.
-
-        May raise `PasswordNotFound`, `PasswordExists` or `KeychainError`
-        exceptions (the first two are subclasses of `KeychainError`).
-
-        :param action: The ``security`` action to call, e.g.
-                           ``add-generic-password``
-        :type action: ``unicode``
-        :param service: Name of the service.
-        :type service: ``unicode``
-        :param account: name of the account the password is for, e.g.
-            "Pinboard"
-        :type account: ``unicode``
-        :param password: the password to secure
-        :type password: ``unicode``
-        :param *args: list of command line arguments to be passed to
-                      ``security``
-        :type *args: `list` or `tuple`
-        :returns: ``(retcode, output)``. ``retcode`` is an `int`, ``output`` a
-                  ``unicode`` string.
-        :rtype: `tuple` (`int`, ``unicode``)
-
-        """
-        cmd = ['security', action, '-s', service, '-a', account] + list(args)
-        p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
-                             stderr=subprocess.STDOUT)
-        stdout, _ = p.communicate()
-        if p.returncode == 44:  # password does not exist
-            raise PasswordNotFound()
-        elif p.returncode == 45:  # password already exists
-            raise PasswordExists()
-        elif p.returncode > 0:
-            err = KeychainError('Unknown Keychain error : %s' % stdout)
-            err.retcode = p.returncode
-            raise err
-        return stdout.strip().decode('utf-8')

+ 0 - 724
alfred/workflow/workflow3.py

@@ -1,724 +0,0 @@
-# encoding: utf-8
-#
-# Copyright (c) 2016 Dean Jackson <[email protected]>
-#
-# MIT Licence. See http://opensource.org/licenses/MIT
-#
-# Created on 2016-06-25
-#
-
-"""An Alfred 3+ version of :class:`~workflow.Workflow`.
-
-:class:`~workflow.Workflow3` supports new features, such as
-setting :ref:`workflow-variables` and
-:class:`the more advanced modifiers <Modifier>` supported by Alfred 3+.
-
-In order for the feedback mechanism to work correctly, it's important
-to create :class:`Item3` and :class:`Modifier` objects via the
-:meth:`Workflow3.add_item()` and :meth:`Item3.add_modifier()` methods
-respectively. If you instantiate :class:`Item3` or :class:`Modifier`
-objects directly, the current :class:`Workflow3` object won't be aware
-of them, and they won't be sent to Alfred when you call
-:meth:`Workflow3.send_feedback()`.
-
-"""
-
-from __future__ import print_function, unicode_literals, absolute_import
-
-import json
-import os
-import sys
-
-from .workflow import ICON_WARNING, Workflow
-
-
-class Variables(dict):
-    """Workflow variables for Run Script actions.
-
-    .. versionadded: 1.26
-
-    This class allows you to set workflow variables from
-    Run Script actions.
-
-    It is a subclass of :class:`dict`.
-
-    >>> v = Variables(username='deanishe', password='hunter2')
-    >>> v.arg = u'output value'
-    >>> print(v)
-
-    See :ref:`variables-run-script` in the User Guide for more
-    information.
-
-    Args:
-        arg (unicode, optional): Main output/``{query}``.
-        **variables: Workflow variables to set.
-
-
-    Attributes:
-        arg (unicode): Output value (``{query}``).
-        config (dict): Configuration for downstream workflow element.
-
-    """
-
-    def __init__(self, arg=None, **variables):
-        """Create a new `Variables` object."""
-        self.arg = arg
-        self.config = {}
-        super(Variables, self).__init__(**variables)
-
-    @property
-    def obj(self):
-        """Return ``alfredworkflow`` `dict`."""
-        o = {}
-        if self:
-            d2 = {}
-            for k, v in self.items():
-                d2[k] = v
-            o['variables'] = d2
-
-        if self.config:
-            o['config'] = self.config
-
-        if self.arg is not None:
-            o['arg'] = self.arg
-
-        return {'alfredworkflow': o}
-
-    def __unicode__(self):
-        """Convert to ``alfredworkflow`` JSON object.
-
-        Returns:
-            unicode: ``alfredworkflow`` JSON object
-
-        """
-        if not self and not self.config:
-            if self.arg:
-                return self.arg
-            else:
-                return u''
-
-        return json.dumps(self.obj)
-
-    def __str__(self):
-        """Convert to ``alfredworkflow`` JSON object.
-
-        Returns:
-            str: UTF-8 encoded ``alfredworkflow`` JSON object
-
-        """
-        return unicode(self).encode('utf-8')
-
-
-class Modifier(object):
-    """Modify :class:`Item3` arg/icon/variables when modifier key is pressed.
-
-    Don't use this class directly (as it won't be associated with any
-    :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
-    to add modifiers to results.
-
-    >>> it = wf.add_item('Title', 'Subtitle', valid=True)
-    >>> it.setvar('name', 'default')
-    >>> m = it.add_modifier('cmd')
-    >>> m.setvar('name', 'alternate')
-
-    See :ref:`workflow-variables` in the User Guide for more information
-    and :ref:`example usage <example-variables>`.
-
-    Args:
-        key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
-        subtitle (unicode, optional): Override default subtitle.
-        arg (unicode, optional): Argument to pass for this modifier.
-        valid (bool, optional): Override item's validity.
-        icon (unicode, optional): Filepath/UTI of icon to use
-        icontype (unicode, optional): Type of icon. See
-            :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
-            for valid values.
-
-    Attributes:
-        arg (unicode): Arg to pass to following action.
-        config (dict): Configuration for a downstream element, such as
-            a File Filter.
-        icon (unicode): Filepath/UTI of icon.
-        icontype (unicode): Type of icon. See
-            :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
-            for valid values.
-        key (unicode): Modifier key (see above).
-        subtitle (unicode): Override item subtitle.
-        valid (bool): Override item validity.
-        variables (dict): Workflow variables set by this modifier.
-
-    """
-
-    def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None,
-                 icontype=None):
-        """Create a new :class:`Modifier`.
-
-        Don't use this class directly (as it won't be associated with any
-        :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
-        to add modifiers to results.
-
-        Args:
-            key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
-            subtitle (unicode, optional): Override default subtitle.
-            arg (unicode, optional): Argument to pass for this modifier.
-            valid (bool, optional): Override item's validity.
-            icon (unicode, optional): Filepath/UTI of icon to use
-            icontype (unicode, optional): Type of icon. See
-                :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
-                for valid values.
-
-        """
-        self.key = key
-        self.subtitle = subtitle
-        self.arg = arg
-        self.valid = valid
-        self.icon = icon
-        self.icontype = icontype
-
-        self.config = {}
-        self.variables = {}
-
-    def setvar(self, name, value):
-        """Set a workflow variable for this Item.
-
-        Args:
-            name (unicode): Name of variable.
-            value (unicode): Value of variable.
-
-        """
-        self.variables[name] = value
-
-    def getvar(self, name, default=None):
-        """Return value of workflow variable for ``name`` or ``default``.
-
-        Args:
-            name (unicode): Variable name.
-            default (None, optional): Value to return if variable is unset.
-
-        Returns:
-            unicode or ``default``: Value of variable if set or ``default``.
-
-        """
-        return self.variables.get(name, default)
-
-    @property
-    def obj(self):
-        """Modifier formatted for JSON serialization for Alfred 3.
-
-        Returns:
-            dict: Modifier for serializing to JSON.
-
-        """
-        o = {}
-
-        if self.subtitle is not None:
-            o['subtitle'] = self.subtitle
-
-        if self.arg is not None:
-            o['arg'] = self.arg
-
-        if self.valid is not None:
-            o['valid'] = self.valid
-
-        if self.variables:
-            o['variables'] = self.variables
-
-        if self.config:
-            o['config'] = self.config
-
-        icon = self._icon()
-        if icon:
-            o['icon'] = icon
-
-        return o
-
-    def _icon(self):
-        """Return `icon` object for item.
-
-        Returns:
-            dict: Mapping for item `icon` (may be empty).
-
-        """
-        icon = {}
-        if self.icon is not None:
-            icon['path'] = self.icon
-
-        if self.icontype is not None:
-            icon['type'] = self.icontype
-
-        return icon
-
-
-class Item3(object):
-    """Represents a feedback item for Alfred 3+.
-
-    Generates Alfred-compliant JSON for a single item.
-
-    Don't use this class directly (as it then won't be associated with
-    any :class:`Workflow3 <workflow.Workflow3>` object), but rather use
-    :meth:`Workflow3.add_item() <workflow.Workflow3.add_item>`.
-    See :meth:`~workflow.Workflow3.add_item` for details of arguments.
-
-    """
-
-    def __init__(self, title, subtitle='', arg=None, autocomplete=None,
-                 match=None, valid=False, uid=None, icon=None, icontype=None,
-                 type=None, largetext=None, copytext=None, quicklookurl=None):
-        """Create a new :class:`Item3` object.
-
-        Use same arguments as for
-        :class:`Workflow.Item <workflow.Workflow.Item>`.
-
-        Argument ``subtitle_modifiers`` is not supported.
-
-        """
-        self.title = title
-        self.subtitle = subtitle
-        self.arg = arg
-        self.autocomplete = autocomplete
-        self.match = match
-        self.valid = valid
-        self.uid = uid
-        self.icon = icon
-        self.icontype = icontype
-        self.type = type
-        self.quicklookurl = quicklookurl
-        self.largetext = largetext
-        self.copytext = copytext
-
-        self.modifiers = {}
-
-        self.config = {}
-        self.variables = {}
-
-    def setvar(self, name, value):
-        """Set a workflow variable for this Item.
-
-        Args:
-            name (unicode): Name of variable.
-            value (unicode): Value of variable.
-
-        """
-        self.variables[name] = value
-
-    def getvar(self, name, default=None):
-        """Return value of workflow variable for ``name`` or ``default``.
-
-        Args:
-            name (unicode): Variable name.
-            default (None, optional): Value to return if variable is unset.
-
-        Returns:
-            unicode or ``default``: Value of variable if set or ``default``.
-
-        """
-        return self.variables.get(name, default)
-
-    def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None,
-                     icontype=None):
-        """Add alternative values for a modifier key.
-
-        Args:
-            key (unicode): Modifier key, e.g. ``"cmd"`` or ``"alt"``
-            subtitle (unicode, optional): Override item subtitle.
-            arg (unicode, optional): Input for following action.
-            valid (bool, optional): Override item validity.
-            icon (unicode, optional): Filepath/UTI of icon.
-            icontype (unicode, optional): Type of icon.  See
-                :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
-                for valid values.
-
-        Returns:
-            Modifier: Configured :class:`Modifier`.
-
-        """
-        mod = Modifier(key, subtitle, arg, valid, icon, icontype)
-
-        # Add Item variables to Modifier
-        mod.variables.update(self.variables)
-
-        self.modifiers[key] = mod
-
-        return mod
-
-    @property
-    def obj(self):
-        """Item formatted for JSON serialization.
-
-        Returns:
-            dict: Data suitable for Alfred 3 feedback.
-
-        """
-        # Required values
-        o = {
-            'title': self.title,
-            'subtitle': self.subtitle,
-            'valid': self.valid,
-        }
-
-        # Optional values
-        if self.arg is not None:
-            o['arg'] = self.arg
-
-        if self.autocomplete is not None:
-            o['autocomplete'] = self.autocomplete
-
-        if self.match is not None:
-            o['match'] = self.match
-
-        if self.uid is not None:
-            o['uid'] = self.uid
-
-        if self.type is not None:
-            o['type'] = self.type
-
-        if self.quicklookurl is not None:
-            o['quicklookurl'] = self.quicklookurl
-
-        if self.variables:
-            o['variables'] = self.variables
-
-        if self.config:
-            o['config'] = self.config
-
-        # Largetype and copytext
-        text = self._text()
-        if text:
-            o['text'] = text
-
-        icon = self._icon()
-        if icon:
-            o['icon'] = icon
-
-        # Modifiers
-        mods = self._modifiers()
-        if mods:
-            o['mods'] = mods
-
-        return o
-
-    def _icon(self):
-        """Return `icon` object for item.
-
-        Returns:
-            dict: Mapping for item `icon` (may be empty).
-
-        """
-        icon = {}
-        if self.icon is not None:
-            icon['path'] = self.icon
-
-        if self.icontype is not None:
-            icon['type'] = self.icontype
-
-        return icon
-
-    def _text(self):
-        """Return `largetext` and `copytext` object for item.
-
-        Returns:
-            dict: `text` mapping (may be empty)
-
-        """
-        text = {}
-        if self.largetext is not None:
-            text['largetype'] = self.largetext
-
-        if self.copytext is not None:
-            text['copy'] = self.copytext
-
-        return text
-
-    def _modifiers(self):
-        """Build `mods` dictionary for JSON feedback.
-
-        Returns:
-            dict: Modifier mapping or `None`.
-
-        """
-        if self.modifiers:
-            mods = {}
-            for k, mod in self.modifiers.items():
-                mods[k] = mod.obj
-
-            return mods
-
-        return None
-
-
-class Workflow3(Workflow):
-    """Workflow class that generates Alfred 3+ feedback.
-
-    It is a subclass of :class:`~workflow.Workflow` and most of its
-    methods are documented there.
-
-    Attributes:
-        item_class (class): Class used to generate feedback items.
-        variables (dict): Top level workflow variables.
-
-    """
-
-    item_class = Item3
-
-    def __init__(self, **kwargs):
-        """Create a new :class:`Workflow3` object.
-
-        See :class:`~workflow.Workflow` for documentation.
-
-        """
-        Workflow.__init__(self, **kwargs)
-        self.variables = {}
-        self._rerun = 0
-        # Get session ID from environment if present
-        self._session_id = os.getenv('_WF_SESSION_ID') or None
-        if self._session_id:
-            self.setvar('_WF_SESSION_ID', self._session_id)
-
-    @property
-    def _default_cachedir(self):
-        """Alfred 4's default cache directory."""
-        return os.path.join(
-            os.path.expanduser(
-                '~/Library/Caches/com.runningwithcrayons.Alfred/'
-                'Workflow Data/'),
-            self.bundleid)
-
-    @property
-    def _default_datadir(self):
-        """Alfred 4's default data directory."""
-        return os.path.join(os.path.expanduser(
-            '~/Library/Application Support/Alfred/Workflow Data/'),
-            self.bundleid)
-
-    @property
-    def rerun(self):
-        """How often (in seconds) Alfred should re-run the Script Filter."""
-        return self._rerun
-
-    @rerun.setter
-    def rerun(self, seconds):
-        """Interval at which Alfred should re-run the Script Filter.
-
-        Args:
-            seconds (int): Interval between runs.
-        """
-        self._rerun = seconds
-
-    @property
-    def session_id(self):
-        """A unique session ID every time the user uses the workflow.
-
-        .. versionadded:: 1.25
-
-        The session ID persists while the user is using this workflow.
-        It expires when the user runs a different workflow or closes
-        Alfred.
-
-        """
-        if not self._session_id:
-            from uuid import uuid4
-            self._session_id = uuid4().hex
-            self.setvar('_WF_SESSION_ID', self._session_id)
-
-        return self._session_id
-
-    def setvar(self, name, value, persist=False):
-        """Set a "global" workflow variable.
-
-        .. versionchanged:: 1.33
-
-        These variables are always passed to downstream workflow objects.
-
-        If you have set :attr:`rerun`, these variables are also passed
-        back to the script when Alfred runs it again.
-
-        Args:
-            name (unicode): Name of variable.
-            value (unicode): Value of variable.
-            persist (bool, optional): Also save variable to ``info.plist``?
-
-        """
-        self.variables[name] = value
-        if persist:
-            from .util import set_config
-            set_config(name, value, self.bundleid)
-            self.logger.debug('saved variable %r with value %r to info.plist',
-                              name, value)
-
-    def getvar(self, name, default=None):
-        """Return value of workflow variable for ``name`` or ``default``.
-
-        Args:
-            name (unicode): Variable name.
-            default (None, optional): Value to return if variable is unset.
-
-        Returns:
-            unicode or ``default``: Value of variable if set or ``default``.
-
-        """
-        return self.variables.get(name, default)
-
-    def add_item(self, title, subtitle='', arg=None, autocomplete=None,
-                 valid=False, uid=None, icon=None, icontype=None, type=None,
-                 largetext=None, copytext=None, quicklookurl=None, match=None):
-        """Add an item to be output to Alfred.
-
-        Args:
-            match (unicode, optional): If you have "Alfred filters results"
-                turned on for your Script Filter, Alfred (version 3.5 and
-                above) will filter against this field, not ``title``.
-
-        See :meth:`Workflow.add_item() <workflow.Workflow.add_item>` for
-        the main documentation and other parameters.
-
-        The key difference is that this method does not support the
-        ``modifier_subtitles`` argument. Use the :meth:`~Item3.add_modifier()`
-        method instead on the returned item instead.
-
-        Returns:
-            Item3: Alfred feedback item.
-
-        """
-        item = self.item_class(title, subtitle, arg, autocomplete,
-                               match, valid, uid, icon, icontype, type,
-                               largetext, copytext, quicklookurl)
-
-        # Add variables to child item
-        item.variables.update(self.variables)
-
-        self._items.append(item)
-        return item
-
-    @property
-    def _session_prefix(self):
-        """Filename prefix for current session."""
-        return '_wfsess-{0}-'.format(self.session_id)
-
-    def _mk_session_name(self, name):
-        """New cache name/key based on session ID."""
-        return self._session_prefix + name
-
-    def cache_data(self, name, data, session=False):
-        """Cache API with session-scoped expiry.
-
-        .. versionadded:: 1.25
-
-        Args:
-            name (str): Cache key
-            data (object): Data to cache
-            session (bool, optional): Whether to scope the cache
-                to the current session.
-
-        ``name`` and ``data`` are the same as for the
-        :meth:`~workflow.Workflow.cache_data` method on
-        :class:`~workflow.Workflow`.
-
-        If ``session`` is ``True``, then ``name`` is prefixed
-        with :attr:`session_id`.
-
-        """
-        if session:
-            name = self._mk_session_name(name)
-
-        return super(Workflow3, self).cache_data(name, data)
-
-    def cached_data(self, name, data_func=None, max_age=60, session=False):
-        """Cache API with session-scoped expiry.
-
-        .. versionadded:: 1.25
-
-        Args:
-            name (str): Cache key
-            data_func (callable): Callable that returns fresh data. It
-                is called if the cache has expired or doesn't exist.
-            max_age (int): Maximum allowable age of cache in seconds.
-            session (bool, optional): Whether to scope the cache
-                to the current session.
-
-        ``name``, ``data_func`` and ``max_age`` are the same as for the
-        :meth:`~workflow.Workflow.cached_data` method on
-        :class:`~workflow.Workflow`.
-
-        If ``session`` is ``True``, then ``name`` is prefixed
-        with :attr:`session_id`.
-
-        """
-        if session:
-            name = self._mk_session_name(name)
-
-        return super(Workflow3, self).cached_data(name, data_func, max_age)
-
-    def clear_session_cache(self, current=False):
-        """Remove session data from the cache.
-
-        .. versionadded:: 1.25
-        .. versionchanged:: 1.27
-
-        By default, data belonging to the current session won't be
-        deleted. Set ``current=True`` to also clear current session.
-
-        Args:
-            current (bool, optional): If ``True``, also remove data for
-                current session.
-
-        """
-        def _is_session_file(filename):
-            if current:
-                return filename.startswith('_wfsess-')
-            return filename.startswith('_wfsess-') \
-                and not filename.startswith(self._session_prefix)
-
-        self.clear_cache(_is_session_file)
-
-    @property
-    def obj(self):
-        """Feedback formatted for JSON serialization.
-
-        Returns:
-            dict: Data suitable for Alfred 3 feedback.
-
-        """
-        items = []
-        for item in self._items:
-            items.append(item.obj)
-
-        o = {'items': items}
-        if self.variables:
-            o['variables'] = self.variables
-        if self.rerun:
-            o['rerun'] = self.rerun
-        return o
-
-    def warn_empty(self, title, subtitle=u'', icon=None):
-        """Add a warning to feedback if there are no items.
-
-        .. versionadded:: 1.31
-
-        Add a "warning" item to Alfred feedback if no other items
-        have been added. This is a handy shortcut to prevent Alfred
-        from showing its fallback searches, which is does if no
-        items are returned.
-
-        Args:
-            title (unicode): Title of feedback item.
-            subtitle (unicode, optional): Subtitle of feedback item.
-            icon (str, optional): Icon for feedback item. If not
-                specified, ``ICON_WARNING`` is used.
-
-        Returns:
-            Item3: Newly-created item.
-
-        """
-        if len(self._items):
-            return
-
-        icon = icon or ICON_WARNING
-        return self.add_item(title, subtitle, icon=icon)
-
-    def send_feedback(self):
-        """Print stored items to console/Alfred as JSON."""
-        if self.debugging:
-            json.dump(self.obj, sys.stdout, indent=2, separators=(',', ': '))
-        else:
-            json.dump(self.obj, sys.stdout)
-        sys.stdout.flush()

+ 5 - 12
scripts/make.js

@@ -73,10 +73,7 @@ const beforeMake = async () => {
   fse.ensureDirSync(dist_dir)
 
   const to_cp = [
-    [
-      path.join(root_dir, 'assets', 'app.png'),
-      path.join(root_dir, 'build', 'assets', 'app.png'),
-    ],
+    [path.join(root_dir, 'assets', 'app.png'), path.join(root_dir, 'build', 'assets', 'app.png')],
   ]
 
   to_cp.map(([src, target]) => {
@@ -160,8 +157,7 @@ const doMake = async () => {
           },
         ],
         sign: false,
-        artifactName:
-          '${productName}_mac_${arch}_${version}(${buildVersion}).${ext}',
+        artifactName: '${productName}_mac_${arch}_${version}(${buildVersion}).${ext}',
       },
       win: {
         icon: 'assets/icon.ico',
@@ -171,17 +167,14 @@ const doMake = async () => {
         installerIcon: 'assets/installer-icon.ico',
         oneClick: false,
         allowToChangeInstallationDirectory: true,
-        artifactName:
-          '${productName}_installer_${arch}_${version}(${buildVersion}).${ext}',
+        artifactName: '${productName}_windows_installer_${arch}_${version}(${buildVersion}).${ext}',
       },
       portable: {
-        artifactName:
-          '${productName}_portable_${arch}_${version}(${buildVersion}).${ext}',
+        artifactName: '${productName}_windows_portable_${arch}_${version}(${buildVersion}).${ext}',
       },
       linux: {
         icon: 'assets/app.icns',
-        artifactName:
-          '${productName}_linux_${arch}_${version}(${buildVersion}).${ext}',
+        artifactName: '${productName}_linux_${arch}_${version}(${buildVersion}).${ext}',
         category: 'Utility',
         synopsis: 'An App for hosts management and switching.',
         desktop: {