Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Script to install/symlink Alfred workflows. Useful for workflow developers.
#!/usr/bin/python
# encoding: utf-8
#
# Copyright (c) 2013 <deanishe@deanishe.net>.
#
# MIT Licence. See http://opensource.org/licenses/MIT
#
# Created on 2013-11-01
#
"""workflow-install [options] [<workflow-directory>...]
Install Alfred workflow(s).
You can specify where to install with -w or in ~/.workflow-install.json
If <workflow-directory> is not specified, the script will search the
current working directory recursively for a workflow (a directory
containing an `info.plist` file).
Usage:
workflow-install [-v|-q|-d] [-s] [-w <directory>] [<workflow-directory>...]
workflow-install (-h|--help)
Options:
-s, --symlink Symlink workflow directory
instead of copying it.
-w, --workflows=<directory> Where to install workflows.
-V, --version Show version number and exit.
-h, --help Show this message and exit.
-q, --quiet Show error messages and above.
-v, --verbose Show info messages and above.
-d, --debug Show debug messages.
"""
from __future__ import print_function, unicode_literals
import sys
import os
import logging
import logging.handlers
import json
import plistlib
import shutil
import subprocess
__version__ = "0.3.0"
__author__ = "deanishe@deanishe.net"
log = None
DEFAULT_LOG_LEVEL = logging.WARNING
# LOGPATH = os.path.expanduser('~/Library/Logs/MyScripts.log')
# LOGSIZE = 1024 * 1024 * 5 # 5 megabytes
CONFIG_PATH = os.path.expanduser('~/.workflow-install.json')
DEFAULT_CONFIG = dict(workflows_directory='')
ALFRED_PREFS = (
os.path.expanduser(
'~/Library/Preferences/com.runningwithcrayons.Alfred-Preferences.plist'),
os.path.expanduser(
'~/Library/Preferences/com.runningwithcrayons.Alfred-Preferences-3.plist'),
)
DEFAULT_DIRS = (
os.path.expanduser('~/Library/Application Support/Alfred'),
os.path.expanduser('~/Library/Application Support/Alfred 3'),
)
class TechnicolorFormatter(logging.Formatter):
"""
Prepend level name to any message not level logging.INFO.
Also, colour!
"""
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(8)
RESET = "\033[0m"
COLOUR_BASE = "\033[1;{:d}m"
BOLD = "\033[1m"
LEVEL_COLOURS = {
logging.DEBUG: BLUE,
logging.INFO: WHITE,
logging.WARNING: YELLOW,
logging.ERROR: MAGENTA,
logging.CRITICAL: RED,
}
def __init__(self, fmt=None, datefmt=None, technicolor=True):
logging.Formatter.__init__(self, fmt, datefmt)
self.technicolor = technicolor
self._isatty = sys.stderr.isatty()
def format(self, record):
if record.levelno == logging.INFO:
msg = logging.Formatter.format(self, record)
return msg
if self.technicolor and self._isatty:
colour = self.LEVEL_COLOURS[record.levelno]
bold = (False, True)[record.levelno > logging.INFO]
levelname = self.colourise('{:9s}'.format(record.levelname),
colour, bold)
else:
levelname = '{:9s}'.format(record.levelname)
return (levelname + logging.Formatter.format(self, record))
def colourise(self, text, colour, bold=False):
colour = self.COLOUR_BASE.format(colour + 30)
output = []
if bold:
output.append(self.BOLD)
output.append(colour)
output.append(text)
output.append(self.RESET)
return ''.join(output)
# console output
console = logging.StreamHandler()
formatter = TechnicolorFormatter('%(message)s')
console.setFormatter(formatter)
console.setLevel(logging.DEBUG)
log = logging.getLogger('')
# log.addHandler(logfile)
log.addHandler(console)
def read_plist(path):
"""Convert plist to XML and read its contents."""
cmd = [b'plutil', b'-convert', b'xml1', b'-o', b'-', path]
xml = subprocess.check_output(cmd)
return plistlib.readPlistFromString(xml)
def get_workflow_directory():
"""Return path to Alfred's workflow directory."""
default_syncdir = None
syncdir = None
for i, path in enumerate(ALFRED_PREFS):
if os.path.exists(path):
log.debug('looking for preferences file %r ...', path)
prefs = read_plist(path)
syncdir = prefs.get('syncfolder')
default_syncdir = DEFAULT_DIRS[i]
break
if not syncdir:
# Alfred preserves syncdir setting even if directory no longer exists.
# In this case, Alfred falls back to using its default directory in
# ~/Library/Application Support
if not syncdir or not os.path.exists(syncdir):
syncdir = default_syncdir
if not syncdir:
log.debug('Alfred sync folder not found')
return None
syncdir = os.path.expanduser(syncdir)
if not os.path.exists(syncdir):
log.debug('Alfred syncdir %r does not exist', syncdir)
return None
wf_dir = os.path.join(syncdir, 'Alfred.alfredpreferences/workflows')
log.debug('workflow sync dir : %r', wf_dir)
if os.path.exists(wf_dir):
log.debug('workflow directory retrieved from Alfred preferences')
return wf_dir
log.debug('Alfred.alfredpreferences/workflows not found')
return None
def find_workflow_dir(dirpath):
"""Recursively search `dirpath` for a workflow.
A workflow is a directory containing an `info.plist` file.
"""
for root, _, filenames in os.walk(dirpath):
if 'info.plist' in filenames:
log.debug('Workflow found at %r', root)
return root
return None
def printable_path(dirpath):
"""Replace $HOME with ~."""
return dirpath.replace(os.getenv('HOME'), '~')
def load_config():
"""Load configuration from file."""
if not os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH, 'wb') as file:
json.dump(DEFAULT_CONFIG, file)
return DEFAULT_CONFIG
with open(CONFIG_PATH) as file:
return json.load(file)
def install_workflow(workflow_dir, install_base, symlink=False):
"""Install workflow at `workflow_dir` under directory `install_base`."""
if symlink:
log.debug("Linking workflow at %r to %r", workflow_dir, install_base)
else:
log.debug("Installing workflow at %r to %r",
workflow_dir, install_base)
infopath = os.path.join(workflow_dir, 'info.plist')
if not os.path.exists(infopath):
log.error('info.plist not found : %s', infopath)
return False
info = plistlib.readPlist(infopath)
name = info['name']
bundleid = info['bundleid']
if not bundleid:
log.error('Bundle ID is not set : %s', infopath)
return False
install_path = os.path.join(install_base, bundleid)
action = ('Installing', 'Linking')[symlink]
log.info('%s workflow `%s` to `%s` ...',
action, name, printable_path(install_path))
# delete existing workflow
if os.path.exists(install_path) or os.path.lexists(install_path):
log.info('Deleting existing workflow ...')
if os.path.islink(install_path) or os.path.isfile(install_path):
os.unlink(install_path)
elif os.path.isdir(install_path):
log.info('Directory : %s', install_path)
shutil.rmtree(install_path)
else:
log.info('Something else : %s', install_path)
os.unlink(install_path)
# Symlink or copy workflow to destination
if symlink:
relpath = os.path.relpath(workflow_dir, os.path.dirname(install_path))
log.debug('relative path : %r', relpath)
os.symlink(relpath, install_path)
else:
shutil.copytree(workflow_dir, install_path)
return True
def main(args=None):
"""Run program."""
from docopt import docopt
args = docopt(__doc__, version=__version__)
if args.get('--verbose'):
log.setLevel(logging.INFO)
elif args.get('--quiet'):
log.setLevel(logging.ERROR)
elif args.get('--debug'):
log.setLevel(logging.DEBUG)
else:
log.setLevel(DEFAULT_LOG_LEVEL)
log.debug("Set log level to %s" %
logging.getLevelName(log.level))
log.debug('args : \n%s', args)
workflows_directory = (args.get('--workflows') or
get_workflow_directory() or
load_config().get('workflows_directory'))
if not workflows_directory:
log.error("You didn't specify where to install the workflow(s).\n"
"Try -w workflow/install/path or -h for more info.")
return 1
workflows_directory = os.path.expanduser(workflows_directory)
# Ensure workflows_directory is Unicode
if not isinstance(workflows_directory, unicode):
workflows_directory = unicode(workflows_directory, 'utf-8')
workflow_paths = args.get('<workflow-directory>')
if not workflow_paths:
cwd = os.getcwd()
wfdir = find_workflow_dir(cwd)
if not wfdir:
log.critical('No workflow found under %r', cwd)
return 1
workflow_paths = [wfdir]
errors = False
for path in workflow_paths:
if not isinstance(path, unicode):
path = unicode(path, 'utf-8')
path = os.path.abspath(path)
if not os.path.exists(path):
log.error('Directory does not exist : %s', path)
continue
if not os.path.isdir(path):
log.error('Not a directory : %s', path)
continue
if not install_workflow(path, workflows_directory,
args.get('--symlink')):
errors = True
if errors:
return 1
return 0
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))
@iandol

This comment has been minimized.

Copy link

iandol commented Sep 9, 2017

Hi @deanishe, I'm getting the following error trying to run this:

  File "/Users/ian/bin/workflow-install.py", line 140, in read_plist
    return plistlib.readPlistFromString(xml)
AttributeError: module 'plistlib' has no attribute 'readPlistFromString'
>  python --version
Python 3.6.2 :: Continuum Analytics, Inc.
@robsalasco

This comment has been minimized.

Copy link

robsalasco commented Oct 15, 2017

@iandol you need python 2

@deanishe

This comment has been minimized.

Copy link
Owner Author

deanishe commented Nov 30, 2017

@iandol I've fixed the shebang.

The problem is you've installed some janky Python that installs Python 3 as python instead of python3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.