Skip to content

Instantly share code, notes, and snippets.

@deanishe
Last active September 21, 2023 15:38
  • Star 42 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
Star You must be signed in to star a gist
Save deanishe/35faae3e7f89f629a94e to your computer and use it in GitHub Desktop.
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 by specifying an Alfred version
with --alfred or a specific directory with -w or in ~/.workflow-install.json
By default, it should install in the latest version of Alfred.
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] [-a <version>] [-s] [-w <directory>]
[<workflow-directory>...]
workflow-install (-h|--help)
Options:
-a, --alfred=<version> version of Alfred to install workflow
in, e.g. "3" or "4"
-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.4.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/Application Support/Alfred/prefs.json')
ALFRED3_PREFS = os.path.expanduser(
'~/Library/Preferences/com.runningwithcrayons.'
'Alfred-Preferences-3.plist')
DEFAULT_DIR = os.path.expanduser('~/Library/Application Support/Alfred')
DEFAULT_DIR3 = 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(version=None):
"""Return path to Alfred's workflow directory."""
dirs = []
if version == '3' or version is None:
dirs.append(DEFAULT_DIR3)
if os.path.exists(ALFRED3_PREFS):
prefs = read_plist(ALFRED3_PREFS)
dirs.append(prefs.get('syncfolder'))
if version != '3':
dirs.append(DEFAULT_DIR)
if os.path.exists(ALFRED_PREFS):
with open(ALFRED_PREFS, 'rb') as fp:
prefs = json.load(fp)
if not version:
s = prefs.get('current')
log.debug('workflow sync dir: %r', s)
return os.path.join(s, 'workflows')
dirs.append(prefs.get('syncfolders', {}).get(version, ''))
for p in dirs[::-1]:
if not p:
continue
p = os.path.expanduser(p)
# 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 os.path.exists(p):
syncdir = p
break
else:
log.debug('Alfred sync folder not found')
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(version=args.get('--alfred'))
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
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
Copy link

@iandol you need python 2

@deanishe
Copy link
Author

@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