workflow3.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  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. """An Alfred 3+ version of :class:`~workflow.Workflow`.
  10. :class:`~workflow.Workflow3` supports new features, such as
  11. setting :ref:`workflow-variables` and
  12. :class:`the more advanced modifiers <Modifier>` supported by Alfred 3+.
  13. In order for the feedback mechanism to work correctly, it's important
  14. to create :class:`Item3` and :class:`Modifier` objects via the
  15. :meth:`Workflow3.add_item()` and :meth:`Item3.add_modifier()` methods
  16. respectively. If you instantiate :class:`Item3` or :class:`Modifier`
  17. objects directly, the current :class:`Workflow3` object won't be aware
  18. of them, and they won't be sent to Alfred when you call
  19. :meth:`Workflow3.send_feedback()`.
  20. """
  21. from __future__ import print_function, unicode_literals, absolute_import
  22. import json
  23. import os
  24. import sys
  25. from .workflow import ICON_WARNING, Workflow
  26. class Variables(dict):
  27. """Workflow variables for Run Script actions.
  28. .. versionadded: 1.26
  29. This class allows you to set workflow variables from
  30. Run Script actions.
  31. It is a subclass of :class:`dict`.
  32. >>> v = Variables(username='deanishe', password='hunter2')
  33. >>> v.arg = u'output value'
  34. >>> print(v)
  35. See :ref:`variables-run-script` in the User Guide for more
  36. information.
  37. Args:
  38. arg (unicode, optional): Main output/``{query}``.
  39. **variables: Workflow variables to set.
  40. Attributes:
  41. arg (unicode): Output value (``{query}``).
  42. config (dict): Configuration for downstream workflow element.
  43. """
  44. def __init__(self, arg=None, **variables):
  45. """Create a new `Variables` object."""
  46. self.arg = arg
  47. self.config = {}
  48. super(Variables, self).__init__(**variables)
  49. @property
  50. def obj(self):
  51. """Return ``alfredworkflow`` `dict`."""
  52. o = {}
  53. if self:
  54. d2 = {}
  55. for k, v in self.items():
  56. d2[k] = v
  57. o['variables'] = d2
  58. if self.config:
  59. o['config'] = self.config
  60. if self.arg is not None:
  61. o['arg'] = self.arg
  62. return {'alfredworkflow': o}
  63. def __unicode__(self):
  64. """Convert to ``alfredworkflow`` JSON object.
  65. Returns:
  66. unicode: ``alfredworkflow`` JSON object
  67. """
  68. if not self and not self.config:
  69. if self.arg:
  70. return self.arg
  71. else:
  72. return u''
  73. return json.dumps(self.obj)
  74. def __str__(self):
  75. """Convert to ``alfredworkflow`` JSON object.
  76. Returns:
  77. str: UTF-8 encoded ``alfredworkflow`` JSON object
  78. """
  79. return unicode(self).encode('utf-8')
  80. class Modifier(object):
  81. """Modify :class:`Item3` arg/icon/variables when modifier key is pressed.
  82. Don't use this class directly (as it won't be associated with any
  83. :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
  84. to add modifiers to results.
  85. >>> it = wf.add_item('Title', 'Subtitle', valid=True)
  86. >>> it.setvar('name', 'default')
  87. >>> m = it.add_modifier('cmd')
  88. >>> m.setvar('name', 'alternate')
  89. See :ref:`workflow-variables` in the User Guide for more information
  90. and :ref:`example usage <example-variables>`.
  91. Args:
  92. key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
  93. subtitle (unicode, optional): Override default subtitle.
  94. arg (unicode, optional): Argument to pass for this modifier.
  95. valid (bool, optional): Override item's validity.
  96. icon (unicode, optional): Filepath/UTI of icon to use
  97. icontype (unicode, optional): Type of icon. See
  98. :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
  99. for valid values.
  100. Attributes:
  101. arg (unicode): Arg to pass to following action.
  102. config (dict): Configuration for a downstream element, such as
  103. a File Filter.
  104. icon (unicode): Filepath/UTI of icon.
  105. icontype (unicode): Type of icon. See
  106. :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
  107. for valid values.
  108. key (unicode): Modifier key (see above).
  109. subtitle (unicode): Override item subtitle.
  110. valid (bool): Override item validity.
  111. variables (dict): Workflow variables set by this modifier.
  112. """
  113. def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None,
  114. icontype=None):
  115. """Create a new :class:`Modifier`.
  116. Don't use this class directly (as it won't be associated with any
  117. :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
  118. to add modifiers to results.
  119. Args:
  120. key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
  121. subtitle (unicode, optional): Override default subtitle.
  122. arg (unicode, optional): Argument to pass for this modifier.
  123. valid (bool, optional): Override item's validity.
  124. icon (unicode, optional): Filepath/UTI of icon to use
  125. icontype (unicode, optional): Type of icon. See
  126. :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
  127. for valid values.
  128. """
  129. self.key = key
  130. self.subtitle = subtitle
  131. self.arg = arg
  132. self.valid = valid
  133. self.icon = icon
  134. self.icontype = icontype
  135. self.config = {}
  136. self.variables = {}
  137. def setvar(self, name, value):
  138. """Set a workflow variable for this Item.
  139. Args:
  140. name (unicode): Name of variable.
  141. value (unicode): Value of variable.
  142. """
  143. self.variables[name] = value
  144. def getvar(self, name, default=None):
  145. """Return value of workflow variable for ``name`` or ``default``.
  146. Args:
  147. name (unicode): Variable name.
  148. default (None, optional): Value to return if variable is unset.
  149. Returns:
  150. unicode or ``default``: Value of variable if set or ``default``.
  151. """
  152. return self.variables.get(name, default)
  153. @property
  154. def obj(self):
  155. """Modifier formatted for JSON serialization for Alfred 3.
  156. Returns:
  157. dict: Modifier for serializing to JSON.
  158. """
  159. o = {}
  160. if self.subtitle is not None:
  161. o['subtitle'] = self.subtitle
  162. if self.arg is not None:
  163. o['arg'] = self.arg
  164. if self.valid is not None:
  165. o['valid'] = self.valid
  166. if self.variables:
  167. o['variables'] = self.variables
  168. if self.config:
  169. o['config'] = self.config
  170. icon = self._icon()
  171. if icon:
  172. o['icon'] = icon
  173. return o
  174. def _icon(self):
  175. """Return `icon` object for item.
  176. Returns:
  177. dict: Mapping for item `icon` (may be empty).
  178. """
  179. icon = {}
  180. if self.icon is not None:
  181. icon['path'] = self.icon
  182. if self.icontype is not None:
  183. icon['type'] = self.icontype
  184. return icon
  185. class Item3(object):
  186. """Represents a feedback item for Alfred 3+.
  187. Generates Alfred-compliant JSON for a single item.
  188. Don't use this class directly (as it then won't be associated with
  189. any :class:`Workflow3 <workflow.Workflow3>` object), but rather use
  190. :meth:`Workflow3.add_item() <workflow.Workflow3.add_item>`.
  191. See :meth:`~workflow.Workflow3.add_item` for details of arguments.
  192. """
  193. def __init__(self, title, subtitle='', arg=None, autocomplete=None,
  194. match=None, valid=False, uid=None, icon=None, icontype=None,
  195. type=None, largetext=None, copytext=None, quicklookurl=None):
  196. """Create a new :class:`Item3` object.
  197. Use same arguments as for
  198. :class:`Workflow.Item <workflow.Workflow.Item>`.
  199. Argument ``subtitle_modifiers`` is not supported.
  200. """
  201. self.title = title
  202. self.subtitle = subtitle
  203. self.arg = arg
  204. self.autocomplete = autocomplete
  205. self.match = match
  206. self.valid = valid
  207. self.uid = uid
  208. self.icon = icon
  209. self.icontype = icontype
  210. self.type = type
  211. self.quicklookurl = quicklookurl
  212. self.largetext = largetext
  213. self.copytext = copytext
  214. self.modifiers = {}
  215. self.config = {}
  216. self.variables = {}
  217. def setvar(self, name, value):
  218. """Set a workflow variable for this Item.
  219. Args:
  220. name (unicode): Name of variable.
  221. value (unicode): Value of variable.
  222. """
  223. self.variables[name] = value
  224. def getvar(self, name, default=None):
  225. """Return value of workflow variable for ``name`` or ``default``.
  226. Args:
  227. name (unicode): Variable name.
  228. default (None, optional): Value to return if variable is unset.
  229. Returns:
  230. unicode or ``default``: Value of variable if set or ``default``.
  231. """
  232. return self.variables.get(name, default)
  233. def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None,
  234. icontype=None):
  235. """Add alternative values for a modifier key.
  236. Args:
  237. key (unicode): Modifier key, e.g. ``"cmd"`` or ``"alt"``
  238. subtitle (unicode, optional): Override item subtitle.
  239. arg (unicode, optional): Input for following action.
  240. valid (bool, optional): Override item validity.
  241. icon (unicode, optional): Filepath/UTI of icon.
  242. icontype (unicode, optional): Type of icon. See
  243. :meth:`Workflow.add_item() <workflow.Workflow.add_item>`
  244. for valid values.
  245. Returns:
  246. Modifier: Configured :class:`Modifier`.
  247. """
  248. mod = Modifier(key, subtitle, arg, valid, icon, icontype)
  249. # Add Item variables to Modifier
  250. mod.variables.update(self.variables)
  251. self.modifiers[key] = mod
  252. return mod
  253. @property
  254. def obj(self):
  255. """Item formatted for JSON serialization.
  256. Returns:
  257. dict: Data suitable for Alfred 3 feedback.
  258. """
  259. # Required values
  260. o = {
  261. 'title': self.title,
  262. 'subtitle': self.subtitle,
  263. 'valid': self.valid,
  264. }
  265. # Optional values
  266. if self.arg is not None:
  267. o['arg'] = self.arg
  268. if self.autocomplete is not None:
  269. o['autocomplete'] = self.autocomplete
  270. if self.match is not None:
  271. o['match'] = self.match
  272. if self.uid is not None:
  273. o['uid'] = self.uid
  274. if self.type is not None:
  275. o['type'] = self.type
  276. if self.quicklookurl is not None:
  277. o['quicklookurl'] = self.quicklookurl
  278. if self.variables:
  279. o['variables'] = self.variables
  280. if self.config:
  281. o['config'] = self.config
  282. # Largetype and copytext
  283. text = self._text()
  284. if text:
  285. o['text'] = text
  286. icon = self._icon()
  287. if icon:
  288. o['icon'] = icon
  289. # Modifiers
  290. mods = self._modifiers()
  291. if mods:
  292. o['mods'] = mods
  293. return o
  294. def _icon(self):
  295. """Return `icon` object for item.
  296. Returns:
  297. dict: Mapping for item `icon` (may be empty).
  298. """
  299. icon = {}
  300. if self.icon is not None:
  301. icon['path'] = self.icon
  302. if self.icontype is not None:
  303. icon['type'] = self.icontype
  304. return icon
  305. def _text(self):
  306. """Return `largetext` and `copytext` object for item.
  307. Returns:
  308. dict: `text` mapping (may be empty)
  309. """
  310. text = {}
  311. if self.largetext is not None:
  312. text['largetype'] = self.largetext
  313. if self.copytext is not None:
  314. text['copy'] = self.copytext
  315. return text
  316. def _modifiers(self):
  317. """Build `mods` dictionary for JSON feedback.
  318. Returns:
  319. dict: Modifier mapping or `None`.
  320. """
  321. if self.modifiers:
  322. mods = {}
  323. for k, mod in self.modifiers.items():
  324. mods[k] = mod.obj
  325. return mods
  326. return None
  327. class Workflow3(Workflow):
  328. """Workflow class that generates Alfred 3+ feedback.
  329. It is a subclass of :class:`~workflow.Workflow` and most of its
  330. methods are documented there.
  331. Attributes:
  332. item_class (class): Class used to generate feedback items.
  333. variables (dict): Top level workflow variables.
  334. """
  335. item_class = Item3
  336. def __init__(self, **kwargs):
  337. """Create a new :class:`Workflow3` object.
  338. See :class:`~workflow.Workflow` for documentation.
  339. """
  340. Workflow.__init__(self, **kwargs)
  341. self.variables = {}
  342. self._rerun = 0
  343. # Get session ID from environment if present
  344. self._session_id = os.getenv('_WF_SESSION_ID') or None
  345. if self._session_id:
  346. self.setvar('_WF_SESSION_ID', self._session_id)
  347. @property
  348. def _default_cachedir(self):
  349. """Alfred 4's default cache directory."""
  350. return os.path.join(
  351. os.path.expanduser(
  352. '~/Library/Caches/com.runningwithcrayons.Alfred/'
  353. 'Workflow Data/'),
  354. self.bundleid)
  355. @property
  356. def _default_datadir(self):
  357. """Alfred 4's default data directory."""
  358. return os.path.join(os.path.expanduser(
  359. '~/Library/Application Support/Alfred/Workflow Data/'),
  360. self.bundleid)
  361. @property
  362. def rerun(self):
  363. """How often (in seconds) Alfred should re-run the Script Filter."""
  364. return self._rerun
  365. @rerun.setter
  366. def rerun(self, seconds):
  367. """Interval at which Alfred should re-run the Script Filter.
  368. Args:
  369. seconds (int): Interval between runs.
  370. """
  371. self._rerun = seconds
  372. @property
  373. def session_id(self):
  374. """A unique session ID every time the user uses the workflow.
  375. .. versionadded:: 1.25
  376. The session ID persists while the user is using this workflow.
  377. It expires when the user runs a different workflow or closes
  378. Alfred.
  379. """
  380. if not self._session_id:
  381. from uuid import uuid4
  382. self._session_id = uuid4().hex
  383. self.setvar('_WF_SESSION_ID', self._session_id)
  384. return self._session_id
  385. def setvar(self, name, value, persist=False):
  386. """Set a "global" workflow variable.
  387. .. versionchanged:: 1.33
  388. These variables are always passed to downstream workflow objects.
  389. If you have set :attr:`rerun`, these variables are also passed
  390. back to the script when Alfred runs it again.
  391. Args:
  392. name (unicode): Name of variable.
  393. value (unicode): Value of variable.
  394. persist (bool, optional): Also save variable to ``info.plist``?
  395. """
  396. self.variables[name] = value
  397. if persist:
  398. from .util import set_config
  399. set_config(name, value, self.bundleid)
  400. self.logger.debug('saved variable %r with value %r to info.plist',
  401. name, value)
  402. def getvar(self, name, default=None):
  403. """Return value of workflow variable for ``name`` or ``default``.
  404. Args:
  405. name (unicode): Variable name.
  406. default (None, optional): Value to return if variable is unset.
  407. Returns:
  408. unicode or ``default``: Value of variable if set or ``default``.
  409. """
  410. return self.variables.get(name, default)
  411. def add_item(self, title, subtitle='', arg=None, autocomplete=None,
  412. valid=False, uid=None, icon=None, icontype=None, type=None,
  413. largetext=None, copytext=None, quicklookurl=None, match=None):
  414. """Add an item to be output to Alfred.
  415. Args:
  416. match (unicode, optional): If you have "Alfred filters results"
  417. turned on for your Script Filter, Alfred (version 3.5 and
  418. above) will filter against this field, not ``title``.
  419. See :meth:`Workflow.add_item() <workflow.Workflow.add_item>` for
  420. the main documentation and other parameters.
  421. The key difference is that this method does not support the
  422. ``modifier_subtitles`` argument. Use the :meth:`~Item3.add_modifier()`
  423. method instead on the returned item instead.
  424. Returns:
  425. Item3: Alfred feedback item.
  426. """
  427. item = self.item_class(title, subtitle, arg, autocomplete,
  428. match, valid, uid, icon, icontype, type,
  429. largetext, copytext, quicklookurl)
  430. # Add variables to child item
  431. item.variables.update(self.variables)
  432. self._items.append(item)
  433. return item
  434. @property
  435. def _session_prefix(self):
  436. """Filename prefix for current session."""
  437. return '_wfsess-{0}-'.format(self.session_id)
  438. def _mk_session_name(self, name):
  439. """New cache name/key based on session ID."""
  440. return self._session_prefix + name
  441. def cache_data(self, name, data, session=False):
  442. """Cache API with session-scoped expiry.
  443. .. versionadded:: 1.25
  444. Args:
  445. name (str): Cache key
  446. data (object): Data to cache
  447. session (bool, optional): Whether to scope the cache
  448. to the current session.
  449. ``name`` and ``data`` are the same as for the
  450. :meth:`~workflow.Workflow.cache_data` method on
  451. :class:`~workflow.Workflow`.
  452. If ``session`` is ``True``, then ``name`` is prefixed
  453. with :attr:`session_id`.
  454. """
  455. if session:
  456. name = self._mk_session_name(name)
  457. return super(Workflow3, self).cache_data(name, data)
  458. def cached_data(self, name, data_func=None, max_age=60, session=False):
  459. """Cache API with session-scoped expiry.
  460. .. versionadded:: 1.25
  461. Args:
  462. name (str): Cache key
  463. data_func (callable): Callable that returns fresh data. It
  464. is called if the cache has expired or doesn't exist.
  465. max_age (int): Maximum allowable age of cache in seconds.
  466. session (bool, optional): Whether to scope the cache
  467. to the current session.
  468. ``name``, ``data_func`` and ``max_age`` are the same as for the
  469. :meth:`~workflow.Workflow.cached_data` method on
  470. :class:`~workflow.Workflow`.
  471. If ``session`` is ``True``, then ``name`` is prefixed
  472. with :attr:`session_id`.
  473. """
  474. if session:
  475. name = self._mk_session_name(name)
  476. return super(Workflow3, self).cached_data(name, data_func, max_age)
  477. def clear_session_cache(self, current=False):
  478. """Remove session data from the cache.
  479. .. versionadded:: 1.25
  480. .. versionchanged:: 1.27
  481. By default, data belonging to the current session won't be
  482. deleted. Set ``current=True`` to also clear current session.
  483. Args:
  484. current (bool, optional): If ``True``, also remove data for
  485. current session.
  486. """
  487. def _is_session_file(filename):
  488. if current:
  489. return filename.startswith('_wfsess-')
  490. return filename.startswith('_wfsess-') \
  491. and not filename.startswith(self._session_prefix)
  492. self.clear_cache(_is_session_file)
  493. @property
  494. def obj(self):
  495. """Feedback formatted for JSON serialization.
  496. Returns:
  497. dict: Data suitable for Alfred 3 feedback.
  498. """
  499. items = []
  500. for item in self._items:
  501. items.append(item.obj)
  502. o = {'items': items}
  503. if self.variables:
  504. o['variables'] = self.variables
  505. if self.rerun:
  506. o['rerun'] = self.rerun
  507. return o
  508. def warn_empty(self, title, subtitle=u'', icon=None):
  509. """Add a warning to feedback if there are no items.
  510. .. versionadded:: 1.31
  511. Add a "warning" item to Alfred feedback if no other items
  512. have been added. This is a handy shortcut to prevent Alfred
  513. from showing its fallback searches, which is does if no
  514. items are returned.
  515. Args:
  516. title (unicode): Title of feedback item.
  517. subtitle (unicode, optional): Subtitle of feedback item.
  518. icon (str, optional): Icon for feedback item. If not
  519. specified, ``ICON_WARNING`` is used.
  520. Returns:
  521. Item3: Newly-created item.
  522. """
  523. if len(self._items):
  524. return
  525. icon = icon or ICON_WARNING
  526. return self.add_item(title, subtitle, icon=icon)
  527. def send_feedback(self):
  528. """Print stored items to console/Alfred as JSON."""
  529. if self.debugging:
  530. json.dump(self.obj, sys.stdout, indent=2, separators=(',', ': '))
  531. else:
  532. json.dump(self.obj, sys.stdout)
  533. sys.stdout.flush()