Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support setting Alfred variables with alfy.output (#43) #44

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const cleanStack = require('clean-stack');
const dotProp = require('dot-prop');
const CacheConf = require('cache-conf');
const updateNotification = require('./lib/update-notification');
const {format} = require('./lib/utils');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Destructuring is not yet supported in Node 4. So just import it as utils and use utils.format below.


const alfy = module.exports;

Expand Down Expand Up @@ -38,8 +39,13 @@ alfy.alfred = {

alfy.input = process.argv[2];

alfy.output = arr => {
console.log(JSON.stringify({items: arr}, null, '\t'));
alfy.output = items => {
if (!Array.isArray(items)) {
throw new TypeError(`Expected \`items\` to be an Array, got \`${typeof items}\``);
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use items.map

items = items.map(item => format(item));

And then return the item in the format function.

items = items.map(item => format(item));
console.log(JSON.stringify({items}, null, '\t'));
};

alfy.matches = (input, list, item) => {
Expand Down Expand Up @@ -159,6 +165,8 @@ alfy.icon = {
delete: getIcon('ToolbarDeleteIcon')
};

alfy.env = process.env;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sindresorhus Should we put this in alfy.meta.env instead?


loudRejection(alfy.error);
process.on('uncaughtException', alfy.error);
hookStd.stderr(alfy.error);
36 changes: 36 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
const isPlainObj = require('is-plain-obj');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'use strict';


const wrapArg = item => {
const alfredworkflow = {arg: item.arg, variables: item.env};
const arg = JSON.stringify({alfredworkflow});
const newItem = Object.assign({}, item, {arg});
delete newItem.env;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we overwrite env with undefined instead in the line above? If I'm not mistaken delete is bad for performance.

return newItem;
};

const formatMods = item => {
const copy = Object.assign({}, item);

for (const mod of Object.keys(item.mods)) {
copy.mods[mod] = wrapArg(item.mods[mod]);
}

return copy;
};

// See https://www.alfredforum.com/topic/9070-how-to-workflowenvironment-variables/ for documentation on setting environment variables from Alfred workflows.
module.exports.format = item => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exports.format

if (!isPlainObj(item)) {
throw new TypeError(`Expected \`item\` to be a plain object, got \`${typeof item}\`.`);
}

if (item.env) {
item = wrapArg(item);
}

if (item.mods) {
item = formatMods(item);
}

return item;
};
Binary file added media/screenshot-variable.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@
],
"dependencies": {
"alfred-link": "^0.2.0",
"alfred-notifier": "^0.2.0",
"alfred-notifier": "^0.2.1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert, semver takes care of this.

"cache-conf": "^0.3.0",
"clean-stack": "^1.0.0",
"clean-stack": "^1.3.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert

"conf": "^0.11.0",
"dot-prop": "^4.0.0",
"dot-prop": "^4.1.1",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert

"execa": "^0.5.0",
"got": "^6.3.0",
"hook-std": "^0.2.0",
"is-plain-obj": "^1.1.0",
"loud-rejection": "^1.6.0",
"npm-run-path": "^2.0.2",
"read-pkg-up": "^1.0.1"
Expand Down
44 changes: 43 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- [Finds the `node` binary.](run-node.sh)
- Presents uncaught exceptions and unhandled Promise rejections to the user.<br>
*No need to manually `.catch()` top-level promises.*
- Easily set [environment variables](#environment-variables).


## Prerequisites
Expand Down Expand Up @@ -165,7 +166,12 @@ Return output to Alfred.

Type: `Array`

List of `Object` with any of the [supported properties](https://www.alfredapp.com/help/workflows/inputs/script-filter/json/).
List of `Object` with any of the [supported properties](https://www.alfredapp.com/help/workflows/inputs/script-filter/json/). If a list item has a `variables` property, it will be used to set Alfred's [Workflow Environment Variables](https://www.alfredapp.com/help/workflows/advanced/variables/) when the user selects the item.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a list item has a `env` property...


See also:

- [Environment variables in Alfy](#environment-variables)
- [How to set environment variables in Alfred Workflows](https://www.alfredforum.com/topic/9070-how-to-workflowenvironment-variables/)

Example:

Expand Down Expand Up @@ -490,6 +496,42 @@ Example: `'adbd4f66bc3ae8493832af61a41ee609b20d8705'`

Non-synced local preferences are stored within `Alfred.alfredpreferences` under `…/preferences/local/${preferencesLocalHash}/`.

## Environment Variables

Alfy makes it easy to set environment variables when a user selects an item:

```js
alfy.output([
{
title: 'Unicorn',
arg: '🦄',
env: {
color: 'white'
}
},
{
title: 'Rainbow',
arg: '🌈',
env: {
color: 'myriad'
}
}
]);
```

You can access Alfred Workflow Variables through `process.env` or `alfy.env`:

```js
// After a user selects "Unicorn" or "Rainbow"
process.env.color
alfy.env.color
//=> 'white' if they selected Unicorn
//=> 'myriad' if they selected Rainbow
```

Alfred Workflow Variables are also available in the workflow editor using the form `{var:varname}`. For example, `{var:color}`

<img src="media/screenshot-variable.png" width="694">

## Users

Expand Down
2 changes: 1 addition & 1 deletion test/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test('expired data', async t => {
t.falsy(m.cache.store.expire);
});

test('versioned data', async t => {
test('versioned data', t => {
const cache = tempfile();

const m = alfy({cache, version: '1.0.0'});
Expand Down
2 changes: 1 addition & 1 deletion test/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ test('no cache', async t => {

test('transform not a function', async t => {
const m = alfy();
t.throws(m.fetch(`${URL}/no-cache`, {transform: 'foo'}), 'Expected `transform` to be a `function`, got `string`');
await t.throws(m.fetch(`${URL}/no-cache`, {transform: 'foo'}), 'Expected `transform` to be a `function`, got `string`');
});

test('transform', async t => {
Expand Down
129 changes: 129 additions & 0 deletions test/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {serial as test} from 'ava';
import hookStd from 'hook-std';
import {alfy} from './_utils';

const itemWithMod = {
title: 'unicorn',
arg: '🦄',
env: {fabulous: true},
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put it on a new line

mods: {
alt: {
title: 'Rainbow',
arg: '🌈',
env: {
color: 'myriad'
}
}
}
};

const m = alfy();
const hook = cb => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return a promise instead of a callback, this way you can just use async/await in your tests.

const unhook = hookStd.stdout({silent: true}, output => {
unhook();
if (cb) {
cb(output);
}
});
};

test.cb('.output() properly wraps item.env', t => {
hook(output => {
const item = JSON.parse(output).items[0];
const arg = JSON.parse(item.arg);
t.deepEqual(arg, {
alfredworkflow: {
arg: '🦄',
variables: {fabulous: true}
}
});
t.end();
});
m.output([{
title: 'unicorn',
arg: '🦄',
env: {fabulous: true}
}]);
});

test.cb('.output() wraps item.env even if item.arg is not defined', t => {
hook(output => {
const item = JSON.parse(output).items[0];
const arg = JSON.parse(item.arg);
t.deepEqual(arg, {
alfredworkflow: {
variables: {fabulous: true}
}
});
t.end();
});
m.output([{
title: 'unicorn',
env: {fabulous: true}
}]);
});

test('.output() throws if it doesn\'t receive an array of plain objects', t => {
hook();
const outputNulls = () => m.output([null, null]);
t.throws(outputNulls, TypeError);
});

test.cb('.output() does not wrap item.arg if item.env is not defined', t => {
hook(output => {
const item = JSON.parse(output).items[0];
t.is(item.arg, '🦄');
t.is(item.env, undefined);
t.end();
});
m.output([{
title: 'unicorn',
arg: '🦄'
}]);
});

test('.output() throws a TypeError if its argument isn\'t an array', t => {
let done = false;
const unhook = hookStd.stdout({silent: true}, () => {
if (done) {
unhook();
}
});
t.throws(() => m.output({}), TypeError);
t.throws(() => m.output('unicorn'), TypeError);
t.throws(() => m.output(null), TypeError);
done = true;
t.throws(() => m.output(undefined), TypeError);
});

test.cb('.output() wraps mod items', t => {
hook(output => {
const item = JSON.parse(output).items[0];
const altArg = JSON.parse(item.mods.alt.arg);
t.deepEqual(altArg, {
alfredworkflow: {
arg: '🌈',
variables: {
color: 'myriad'
}
}
});
t.end();
});
m.output([itemWithMod]);
});

test.cb('.output() doesn\'t change original item when mod items are present', t => {
hook(output => {
const item = JSON.parse(output).items[0];
const arg = JSON.parse(item.arg);
t.deepEqual(arg, {
alfredworkflow: {
arg: '🦄',
variables: {fabulous: true}
}
});
t.end();
});
m.output([itemWithMod]);
});