Source code for isheetyounot.aw3

#!/usr/bin/env python
# encoding: utf-8
#
# Copyright (c) 2016 Dean Jackson <deanishe@deanishe.net>
#
# MIT Licence. See http://opensource.org/licenses/MIT
#
# Created on 2016-05-21
#

"""Lightweight Alfred 3+ workflow library."""

from __future__ import print_function, unicode_literals, absolute_import

import json
import os
import sys
import time
from uuid import uuid4


# System error-type icon. Used by rescue() on error.
ICON_ERROR = ('/System/Library/CoreServices/CoreTypes.bundle/Contents/'
              'Resources/AlertStopIcon.icns')


[docs]def log(s, *args): """Print message `s` to STDERR. Run `s % args` with any `args`. Args: s (unicode): Message to print/format string. *args (object): If given, used in format string `s % args`. """ if args: s = s % args if isinstance(s, unicode): s = s.encode('utf-8') print(s, file=sys.stderr)
[docs]def human_time(seconds): """Human-readable duration, e.g. 5m2s or 1h2m. Args: seconds (float): Number of seconds. Returns: unicode: Human-readable duration. """ s = seconds if s < 5: return '{:0.2f}s'.format(s) if s < 60: return '{:0.0f}s'.format(s) m, s = divmod(s, 60) if m < 60: return '{:0.0f}m{:0.0f}s'.format(m, s) h, m = divmod(m, 60) if h < 24: return '{:0.0f}h{:0.0f}m'.format(h, m) d, h = divmod(h, 24) return '{:0.0f}d{:0.0f}h{:0.0f}m'.format(d, h, m)
def _find_upwards(fname): """Find file named `fname` in the current directory or above. Args: fname (unicode): Filename Returns: unicode: Absolute path to file or `None`. """ dirpath = os.path.abspath(os.path.dirname(__file__)) while True: p = os.path.join(dirpath, fname) if os.path.exists(p): log('Found `%s` at `%s`', fname, p) return p if dirpath == '/': return None dirpath = os.path.dirname(dirpath)
[docs]def run_command(cmd): """Run command and return output. Args: cmd (sequence): Sequence or arguments to pass to `Popen` Returns: tuple: `(stdout, stderr)` Raises: CalledProcessError: Raised if command exits with non-zero status """ from subprocess import Popen, CalledProcessError p = Popen(cmd) stdout, stderr = p.communicate() if p.returncode != 0: raise CalledProcessError(p.returncode, cmd) return (stdout, stderr)
[docs]class AttrDict(dict): """Dictionary whose keys are also accessible as attributes.""" def __init__(self, *args, **kwargs): """Create new `AttrDict`. Args: *args (objects): Arguments to `dict.__init__()` **kwargs (objects): Keyword arguments to `dict.__init__()` """ super(AttrDict, self).__init__(*args, **kwargs) def __getattr__(self, key): """Look up attribute as dictionary key. Args: key (str): Dictionary key/attribute name. Returns: obj: Dictionary value for `key`. Raises: AttributeError: Raised if `key` isn't in dictionary. """ if key not in self: raise AttributeError( "AttrDict object has no attribute '%s'" % key) return self[key] def __setattr__(self, key, value): """Add `value` to the dictionary under `key`. Args: key (str): Dictionary key/attribute name. value (obj): Value to store for `key`. """ self[key] = value
[docs]def alfred_vars(): """Dict of Alfred's environment variables w/out ``alfred_`` prefix.""" d = AttrDict() for k in os.environ: if k.startswith('alfred_'): d[k[7:]] = os.environ[k].decode('utf-8') return d
av = alfred_vars()
[docs]class Feedback(object): """Alfred 3 JSON results. Attributes: items (list): Sequence of `dicts` as generated by `make_item()`. """ def __init__(self, items=None): """Create new `Feedback` object. Args: items (list, optional): Initial items. """ # self.vars = {} # self.config = {} self.items = items or [] def __str__(self): """Alfred 3 JSON format.""" return json.dumps({'items': self.items}, indent=2)
[docs] def send(self): """Send self as results to Alfred 3.""" print(str(self))
[docs]def make_item(title, subtitle=u'', arg=None, icon=None, match=None, **wfvars): """Create new Alfred 3 result. Args: title (unicode): Title of the result. subtitle (unicode, optional): Subtitle of the result. arg (unicode, optional): Arg (value) of the result. icon (unicode, optional): Path to icon for result. match (str, optional): Match field. **wfvars (dict): Unicode values to set as Alfred workflow variables with this result. Returns: dict: Alfred result. """ it = { 'title': title, 'subtitle': subtitle, # 'autocomplete': title, 'text': { 'copy': title, 'largetype': title, }, 'valid': False, } if arg: it['arg'] = arg it['valid'] = True it['text'] = { 'copy': arg, 'largetype': arg, } if match: it['match'] = match if icon is not None: it['icon'] = {'path': icon} if wfvars: payload = {'alfredworkflow': {'arg': it.get('arg'), 'variables': wfvars}} it['arg'] = json.dumps(payload) return it
[docs]def rescue(fn, help_url=None): """Wrap callable `fn`, and catch and log any exceptions it raises. Any captured exception is logged to STDERR and also sent to Alfred as a result. Args: fn (callable): Function/method to call in try ... except block. help_url (unicode, optional): URL to show when an exception is caught. """ st = time.time() try: fn() except Exception as err: from traceback import print_exc log('################ FATAL ERROR ##################') print_exc(file=sys.stderr) log('################# END ERROR ###################') if help_url: log("find assistance at: %s", help_url) # log('%r\n%s', sys.exc_info()[2], err) fb = Feedback() fb.items = [make_item('Fatal error in workflow', unicode(err), icon=ICON_ERROR)] print(fb) log('--------------- %0.3fs elapsed ---------------', time.time() - st)
[docs]def random_bundle_id(prefix=None): """Generate random bundle ID based on UUID4. Args: prefix (unicode, optional): Prefix for the new bundle ID. Returns: unicode: Random bundle ID of form `prefix` + `UUID4`. """ return (prefix or '') + uuid4().hex
[docs]def change_bundle_id(newid): """Change the bundle ID of the current workflow. WARNING: The change will not apply for the current run of the workflow. Args: newid (unicode): New bundle ID. """ ip = _find_upwards('info.plist') pbcmd = 'Set :bundleid ' + newid cmd = ['/usr/libexec/PlistBuddy', '-c', pbcmd, ip] log('cmd=%r', cmd) run_command(cmd)