-
-
Save gennaios/b45d27b2dbf82909c28d2837236223b0 to your computer and use it in GitHub Desktop.
Script to install/symlink Alfred workflows. Useful for workflow developers.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# encoding: utf-8 | |
# | |
# Copyright (c) 2013 [email protected]. | |
# | |
# 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.1" | |
__author__ = "[email protected]" | |
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-3.plist') | |
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) | |
# logfile | |
# logfile = logging.handlers.RotatingFileHandler(LOGPATH, maxBytes=LOGSIZE, | |
# backupCount=5) | |
# formatter = logging.Formatter( | |
# '%(asctime)s %(levelname)-8s [%(name)-12s] %(message)s', | |
# datefmt="%d/%m %H:%M:%S") | |
# logfile.setFormatter(formatter) | |
# logfile.setLevel(logging.DEBUG) | |
# 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.""" | |
prefs = read_plist(ALFRED_PREFS) | |
syncdir = prefs.get('syncfolder') | |
if not syncdir: | |
log.debug('Alfred sync folder not found') | |
return None | |
syncdir = os.path.expanduser(syncdir) | |
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, dirnames, 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}".format( | |
workflow_dir, install_base)) | |
else: | |
log.debug("Installing workflow at {!r} to {!r}".format( | |
workflow_dir, install_base)) | |
infopath = os.path.join(workflow_dir, 'info.plist') | |
if not os.path.exists(infopath): | |
log.error('info.plist not found : {}'.format(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 : {}'.format(install_path)) | |
shutil.rmtree(install_path) | |
else: | |
log.info('Something else : {}'.format(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{}'.format(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 : {}'.format(path)) | |
continue | |
if not os.path.isdir(path): | |
log.error('Not a directory : {}'.format(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:])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment