workflow3.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. # encoding: utf-8
  2. #
  3. # Copyright (c) 2016 Dean Jackson <[email protected]>
  4. #
  5. # MIT Licence. See http://opensource.org/licenses/MIT
  6. #
  7. # Created on 2016-06-25
  8. #
  9. """
  10. :class:`Workflow3` supports Alfred 3's new features.
  11. It is an Alfred 3-only version of :class:`~workflow.workflow.Workflow`.
  12. It supports setting :ref:`workflow-variables` and
  13. :class:`the more advanced modifiers <Modifier>` supported by Alfred 3.
  14. In order for the feedback mechanism to work correctly, it's important
  15. to create :class:`Item3` and :class:`Modifier` objects via the
  16. :meth:`Workflow3.add_item()` and :meth:`Item3.add_modifier()` methods
  17. respectively. If you instantiate :class:`Item3` or :class:`Modifier`
  18. objects directly, the current :class:`~workflow.workflow3.Workflow3`
  19. object won't be aware of them, and they won't be sent to Alfred when
  20. you call :meth:`~workflow.workflow3.Workflow3.send_feedback()`.
  21. """
  22. from __future__ import print_function, unicode_literals, absolute_import
  23. import json
  24. import os
  25. import sys
  26. from .workflow import Workflow
  27. class Modifier(object):
  28. """Modify ``Item3`` values for when specified modifier keys are pressed.
  29. Valid modifiers (i.e. values for ``key``) are:
  30. * cmd
  31. * alt
  32. * shift
  33. * ctrl
  34. * fn
  35. Attributes:
  36. arg (unicode): Arg to pass to following action.
  37. key (unicode): Modifier key (see above).
  38. subtitle (unicode): Override item subtitle.
  39. valid (bool): Override item validity.
  40. variables (dict): Workflow variables set by this modifier.
  41. """
  42. def __init__(self, key, subtitle=None, arg=None, valid=None):
  43. """Create a new :class:`Modifier`.
  44. You probably don't want to use this class directly, but rather
  45. use :meth:`Item3.add_modifier()` to add modifiers to results.
  46. Args:
  47. key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
  48. subtitle (unicode, optional): Override default subtitle.
  49. arg (unicode, optional): Argument to pass for this modifier.
  50. valid (bool, optional): Override item's validity.
  51. """
  52. self.key = key
  53. self.subtitle = subtitle
  54. self.arg = arg
  55. self.valid = valid
  56. self.config = {}
  57. self.variables = {}
  58. def setvar(self, name, value):
  59. """Set a workflow variable for this Item.
  60. Args:
  61. name (unicode): Name of variable.
  62. value (unicode): Value of variable.
  63. """
  64. self.variables[name] = value
  65. def getvar(self, name, default=None):
  66. """Return value of workflow variable for ``name`` or ``default``.
  67. Args:
  68. name (unicode): Variable name.
  69. default (None, optional): Value to return if variable is unset.
  70. Returns:
  71. unicode or ``default``: Value of variable if set or ``default``.
  72. """
  73. return self.variables.get(name, default)
  74. @property
  75. def obj(self):
  76. """Modifier formatted for JSON serialization for Alfred 3.
  77. Returns:
  78. dict: Modifier for serializing to JSON.
  79. """
  80. o = {}
  81. if self.subtitle is not None:
  82. o['subtitle'] = self.subtitle
  83. if self.arg is not None:
  84. o['arg'] = self.arg
  85. if self.valid is not None:
  86. o['valid'] = self.valid
  87. # Variables and config
  88. if self.variables or self.config:
  89. d = {}
  90. if self.variables:
  91. d['variables'] = self.variables
  92. if self.config:
  93. d['config'] = self.config
  94. if self.arg is not None:
  95. d['arg'] = self.arg
  96. o['arg'] = json.dumps({'alfredworkflow': d})
  97. return o
  98. class Item3(object):
  99. """Represents a feedback item for Alfred 3.
  100. Generates Alfred-compliant JSON for a single item.
  101. You probably shouldn't use this class directly, but via
  102. :meth:`Workflow3.add_item`. See :meth:`~Workflow3.add_item`
  103. for details of arguments.
  104. """
  105. def __init__(self, title, subtitle='', arg=None, autocomplete=None,
  106. valid=False, uid=None, icon=None, icontype=None,
  107. type=None, largetext=None, copytext=None, quicklookurl=None):
  108. """Use same arguments as for :meth:`Workflow.add_item`.
  109. Argument ``subtitle_modifiers`` is not supported.
  110. """
  111. self.title = title
  112. self.subtitle = subtitle
  113. self.arg = arg
  114. self.autocomplete = autocomplete
  115. self.valid = valid
  116. self.uid = uid
  117. self.icon = icon
  118. self.icontype = icontype
  119. self.type = type
  120. self.quicklookurl = quicklookurl
  121. self.largetext = largetext
  122. self.copytext = copytext
  123. self.modifiers = {}
  124. self.config = {}
  125. self.variables = {}
  126. def setvar(self, name, value):
  127. """Set a workflow variable for this Item.
  128. Args:
  129. name (unicode): Name of variable.
  130. value (unicode): Value of variable.
  131. """
  132. self.variables[name] = value
  133. def getvar(self, name, default=None):
  134. """Return value of workflow variable for ``name`` or ``default``.
  135. Args:
  136. name (unicode): Variable name.
  137. default (None, optional): Value to return if variable is unset.
  138. Returns:
  139. unicode or ``default``: Value of variable if set or ``default``.
  140. """
  141. return self.variables.get(name, default)
  142. def add_modifier(self, key, subtitle=None, arg=None, valid=None):
  143. """Add alternative values for a modifier key.
  144. Args:
  145. key (unicode): Modifier key, e.g. ``"cmd"`` or ``"alt"``
  146. subtitle (unicode, optional): Override item subtitle.
  147. arg (unicode, optional): Input for following action.
  148. valid (bool, optional): Override item validity.
  149. Returns:
  150. Modifier: Configured :class:`Modifier`.
  151. """
  152. mod = Modifier(key, subtitle, arg, valid)
  153. for k in self.variables:
  154. mod.setvar(k, self.variables[k])
  155. self.modifiers[key] = mod
  156. return mod
  157. @property
  158. def obj(self):
  159. """Item formatted for JSON serialization.
  160. Returns:
  161. dict: Data suitable for Alfred 3 feedback.
  162. """
  163. # Basic values
  164. o = {'title': self.title,
  165. 'subtitle': self.subtitle,
  166. 'valid': self.valid}
  167. icon = {}
  168. # Optional values
  169. if self.arg is not None:
  170. o['arg'] = self.arg
  171. if self.autocomplete is not None:
  172. o['autocomplete'] = self.autocomplete
  173. if self.uid is not None:
  174. o['uid'] = self.uid
  175. if self.type is not None:
  176. o['type'] = self.type
  177. if self.quicklookurl is not None:
  178. o['quicklookurl'] = self.quicklookurl
  179. # Largetype and copytext
  180. text = self._text()
  181. if text:
  182. o['text'] = text
  183. icon = self._icon()
  184. if icon:
  185. o['icon'] = icon
  186. # Variables and config
  187. js = self._vars_and_config()
  188. if js:
  189. o['arg'] = js
  190. # Modifiers
  191. mods = self._modifiers()
  192. if mods:
  193. o['mods'] = mods
  194. return o
  195. def _icon(self):
  196. """Return `icon` object for item.
  197. Returns:
  198. dict: Mapping for item `icon` (may be empty).
  199. """
  200. icon = {}
  201. if self.icon is not None:
  202. icon['path'] = self.icon
  203. if self.icontype is not None:
  204. icon['type'] = self.icontype
  205. return icon
  206. def _text(self):
  207. """Return `largetext` and `copytext` object for item.
  208. Returns:
  209. dict: `text` mapping (may be empty)
  210. """
  211. text = {}
  212. if self.largetext is not None:
  213. text['largetype'] = self.largetext
  214. if self.copytext is not None:
  215. text['copy'] = self.copytext
  216. return text
  217. def _vars_and_config(self):
  218. """Build `arg` including workflow variables and configuration.
  219. Returns:
  220. str: JSON string value for `arg` (or `None`)
  221. """
  222. if self.variables or self.config:
  223. d = {}
  224. if self.variables:
  225. d['variables'] = self.variables
  226. if self.config:
  227. d['config'] = self.config
  228. if self.arg is not None:
  229. d['arg'] = self.arg
  230. return json.dumps({'alfredworkflow': d})
  231. return None
  232. def _modifiers(self):
  233. """Build `mods` dictionary for JSON feedback.
  234. Returns:
  235. dict: Modifier mapping or `None`.
  236. """
  237. if self.modifiers:
  238. mods = {}
  239. for k, mod in self.modifiers.items():
  240. mods[k] = mod.obj
  241. return mods
  242. return None
  243. class Workflow3(Workflow):
  244. """Workflow class that generates Alfred 3 feedback.
  245. Attributes:
  246. item_class (class): Class used to generate feedback items.
  247. variables (dict): Top level workflow variables.
  248. """
  249. item_class = Item3
  250. def __init__(self, **kwargs):
  251. """Create a new :class:`Workflow3` object.
  252. See :class:`~workflow.workflow.Workflow` for documentation.
  253. """
  254. Workflow.__init__(self, **kwargs)
  255. self.variables = {}
  256. self._rerun = 0
  257. self._session_id = None
  258. @property
  259. def _default_cachedir(self):
  260. """Alfred 3's default cache directory."""
  261. return os.path.join(
  262. os.path.expanduser(
  263. '~/Library/Caches/com.runningwithcrayons.Alfred-3/'
  264. 'Workflow Data/'),
  265. self.bundleid)
  266. @property
  267. def _default_datadir(self):
  268. """Alfred 3's default data directory."""
  269. return os.path.join(os.path.expanduser(
  270. '~/Library/Application Support/Alfred 3/Workflow Data/'),
  271. self.bundleid)
  272. @property
  273. def rerun(self):
  274. """How often (in seconds) Alfred should re-run the Script Filter."""
  275. return self._rerun
  276. @rerun.setter
  277. def rerun(self, seconds):
  278. """Interval at which Alfred should re-run the Script Filter.
  279. Args:
  280. seconds (int): Interval between runs.
  281. """
  282. self._rerun = seconds
  283. @property
  284. def session_id(self):
  285. """A unique session ID every time the user uses the workflow.
  286. .. versionadded:: 1.25
  287. The session ID persists while the user is using this workflow.
  288. It expires when the user runs a different workflow or closes
  289. Alfred.
  290. """
  291. if not self._session_id:
  292. sid = os.getenv('_WF_SESSION_ID')
  293. if not sid:
  294. from uuid import uuid4
  295. sid = uuid4().hex
  296. self.setvar('_WF_SESSION_ID', sid)
  297. self._session_id = sid
  298. return self._session_id
  299. def setvar(self, name, value):
  300. """Set a "global" workflow variable.
  301. These variables are always passed to downstream workflow objects.
  302. If you have set :attr:`rerun`, these variables are also passed
  303. back to the script when Alfred runs it again.
  304. Args:
  305. name (unicode): Name of variable.
  306. value (unicode): Value of variable.
  307. """
  308. self.variables[name] = value
  309. def getvar(self, name, default=None):
  310. """Return value of workflow variable for ``name`` or ``default``.
  311. Args:
  312. name (unicode): Variable name.
  313. default (None, optional): Value to return if variable is unset.
  314. Returns:
  315. unicode or ``default``: Value of variable if set or ``default``.
  316. """
  317. return self.variables.get(name, default)
  318. def add_item(self, title, subtitle='', arg=None, autocomplete=None,
  319. valid=False, uid=None, icon=None, icontype=None,
  320. type=None, largetext=None, copytext=None, quicklookurl=None):
  321. """Add an item to be output to Alfred.
  322. See :meth:`~workflow.workflow.Workflow.add_item` for the main
  323. documentation.
  324. The key difference is that this method does not support the
  325. ``modifier_subtitles`` argument. Use the :meth:`~Item3.add_modifier()`
  326. method instead on the returned item instead.
  327. Returns:
  328. Item3: Alfred feedback item.
  329. """
  330. item = self.item_class(title, subtitle, arg,
  331. autocomplete, valid, uid, icon, icontype, type,
  332. largetext, copytext, quicklookurl)
  333. self._items.append(item)
  334. return item
  335. def _mk_session_name(self, name):
  336. """New cache name/key based on session ID."""
  337. return '_wfsess-{0}-{1}'.format(self.session_id, name)
  338. def cache_data(self, name, data, session=False):
  339. """Cache API with session-scoped expiry.
  340. .. versionadded:: 1.25
  341. Args:
  342. name (str): Cache key
  343. data (object): Data to cache
  344. session (bool, optional): Whether to scope the cache
  345. to the current session.
  346. ``name`` and ``data`` are as for the
  347. :meth:`~workflow.workflow.Workflow.cache_data` on
  348. :class:`~workflow.workflow.Workflow`.
  349. If ``session`` is ``True``, the ``name`` variable is prefixed
  350. with :attr:`session_id`.
  351. """
  352. if session:
  353. name = self._mk_session_name(name)
  354. return super(Workflow3, self).cache_data(name, data)
  355. def cached_data(self, name, data_func=None, max_age=60, session=False):
  356. """Cache API with session-scoped expiry.
  357. .. versionadded:: 1.25
  358. Args:
  359. name (str): Cache key
  360. data_func (callable): Callable that returns fresh data. It
  361. is called if the cache has expired or doesn't exist.
  362. max_age (int): Maximum allowable age of cache in seconds.
  363. session (bool, optional): Whether to scope the cache
  364. to the current session.
  365. ``name``, ``data_func`` and ``max_age`` are as for the
  366. :meth:`~workflow.workflow.Workflow.cached_data` on
  367. :class:`~workflow.workflow.Workflow`.
  368. If ``session`` is ``True``, the ``name`` variable is prefixed
  369. with :attr:`session_id`.
  370. """
  371. if session:
  372. name = self._mk_session_name(name)
  373. return super(Workflow3, self).cached_data(name, data_func, max_age)
  374. def clear_session_cache(self):
  375. """Remove *all* session data from the cache.
  376. .. versionadded:: 1.25
  377. """
  378. def _is_session_file(filename):
  379. return filename.startswith('_wfsess-')
  380. self.clear_cache(_is_session_file)
  381. @property
  382. def obj(self):
  383. """Feedback formatted for JSON serialization.
  384. Returns:
  385. dict: Data suitable for Alfred 3 feedback.
  386. """
  387. items = []
  388. for item in self._items:
  389. items.append(item.obj)
  390. o = {'items': items}
  391. if self.variables:
  392. o['variables'] = self.variables
  393. if self.rerun:
  394. o['rerun'] = self.rerun
  395. return o
  396. def send_feedback(self):
  397. """Print stored items to console/Alfred as JSON."""
  398. json.dump(self.obj, sys.stdout)
  399. sys.stdout.flush()