-
-
Notifications
You must be signed in to change notification settings - Fork 121
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
Support setting Alfred variables with alfy.output (#43) #44
Conversation
Hi @noise-machines, thank you for the PR! Really appreciate new contributions :). I tried this out first by looking how you pass down environment variables. It doesn't look like what you're trying to do is supported. This is what I tried and it looks like you are trying to do the same thing alfy.output([
title: 'Hello World',
arg: JSON.stringify({arg: 'foo', variables: {foo: 'bar'}})
]); I executed this but the only thing I get is just the stringified object as argument. It's not being parsed. So I dived into the documentation and found this page and more specifically, the |
Hi @SamVerschueren, Thanks for the feedback! I discovered this feature through this excellent forum post. The trick is to stringify an object that has an alfy.output([{
title: 'Hello World',
arg: JSON.stringify({
alfredworkflow: { arg: 'foo', variables: { foo: 'bar' } }
})
}]); Here's a demo of it working on my machine with Alfred 3.3.1: If you think it'd be helpful, I can update my PR to include references to that forum post where appropriate. |
Aha, alright. Let me check it later today! Thanks again for working on it, it's pretty cool stuff. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall looks good! Works great and I actually need this in one of my workflows, didn't know it was possible :).
Added some comments, just push the changes on the same branch and it will automatically pick it up.
index.js
Outdated
}; | ||
|
||
alfy.output = items => { | ||
if (isDefined(items) && items.forEach) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just check if items is an array, otherwise throw a TypeError
if (!Array.isArray(items)) {
throw new TypeError(`Expected \`items\` to be of type \`Array\`, got \`${typeof items}\``);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This would be a breaking change to the output
API, right?
Old implementation:
alfy.output('hi!'); // no problem
New implementation:
alfy.output('hi!'); // TypeError
I'm happy to implement it, just want to confirm.
P.S. - does Alfy follow SemVer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, I've just changed it to this:
if (Array.isArray(items)) {
items = items.map(format);
}
Let me know if you'd prefer the breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was always documented as accepting an Array. Enforcing the documented behavior is not a breaking change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree. I don't think Alfred accepts anything else except an Array of items. This means that we are now just more verbose to what it accepts.
readme.md
Outdated
Using the `variables` property: | ||
|
||
```js | ||
alfy.output([{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put the objects on newlines
alfy.output([
{
title: 'Unicorn',
arg: '🦄',
variables: {
color: 'white'
}
},
{
//...
}
]);
readme.md
Outdated
//=> 'myriad' if they selected Rainbow | ||
``` | ||
|
||
Alfred Workflow Variables are also available in the workflow editor using the form `{var:varname}`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alfred Workflow Variables are also available in the workflow editor using the form
{var:varname}
.
Can you add an example for the color case above? Would {myColor:color}
work for instance?
if (isDefined(items) && items.forEach) { | ||
items.forEach(formatItemArg); | ||
} | ||
|
There was a problem hiding this comment.
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.
index.js
Outdated
const isDefined = x => x !== null && x !== undefined; | ||
|
||
const formatItemArg = item => { | ||
if (isDefined(item) && isDefined(item.variables)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has to be expanded. It's also possible to provide variables for mods like this
alfy.output([
{
title: 'Unicorn',
arg: '🦄',
variables: {
color: 'white'
},
mods: {
alt: {
title: 'Rainbow',
arg: '🌈',
variables: {
color: 'red'
}
}
}
}
]);
If you press the alt key, it will now show the Rainbow
instead.
test/output.js
Outdated
@@ -0,0 +1,76 @@ | |||
import test from 'ava'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this be import {serial as test} from 'ava';
to make sure it runs all the tests serially. Otherwise the output could be mangled. It's weird that it works right now to be honest.
- handle mod keys - implement tests for mod keys - check if items is an array instead of checking existence of items.forEach - change items.forEach to items.map - fix object formatting in readme - add example of referencing an environment variable in the workflow editor - run tests serially
Thanks for the excellent and in-depth feedback, @SamVerschueren! I believe I got all your requested changes. Let me know if there's anything else I can do. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for all the comments. I like to review in multiple phases :).
Feel free to discuss some of my comments. Maybe you did some things because of a reason. Don't be afraid to say so.
I think we should also put those new format function in a lib/util.js
file.
I was wondering earlier today if we should call it env
instead of variables
. Any suggestions or feedback on this?
index.js
Outdated
|
||
alfy.output = items => { | ||
if (Array.isArray(items)) { | ||
items = items.map(format); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's better to use items.map(item => format(item))
. The reason for this is that the map
callback returns multiple parameters. This results in sending all those parameters to format
which may result in bugs in the future if for instance format starts excepting additional input arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, didn't think about that! Good point. Fixed 😄
readme.md
Outdated
@@ -179,6 +179,40 @@ alfy.output([{ | |||
|
|||
<img src="media/screenshot-output.png" width="694"> | |||
|
|||
Using the `variables` property: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should put this under it's own section so we can easily link it from the highlights and to make it more clear. Not sure where though, any ideas @sindresorhus? Maybe a new section Environment variables
under Usage
or Example
? Or on the same level as those?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new top-level section called "Environment variables".
index.js
Outdated
}; | ||
|
||
alfy.output = items => { | ||
if (Array.isArray(items)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To come back to the throw new TypeError()
thing. Just wondering, is there a reason why you would use alfy.output
without providing an Array
? If not, I think I like to be more explicit about it and throw an error instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did a little more research into this question, and I agree with you about throwing the TypeError, @SamVerschueren. I was misremembering the previous behavior of alfy.output
. It already assumes you've passed an Array
:
// previous implementation
alfy.output = arr => {
console.log(JSON.stringify({items: arr}, null, '\t'));
};
It's true that the previous implementation wouldn't throw a TypeError, but I think the error is actually better. It prevents people from doing stuff like
alfy.output('hi!');
// outputs '{"items":"hi!"}'
which doesn't make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If @sindresorhus agrees, let's move forward with throwing a TypeError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, should throw a TypeError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
index.js
Outdated
// see https://www.alfredforum.com/topic/9070-how-to-workflowenvironment-variables/ | ||
// for documentation on setting environment variables from Alfred workflows | ||
const format = item => { | ||
if (!isDefined(item)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if item
is not defined, we should make it an empty object instead.
item = item || {};
And just continue. This will make sure that we don't output null
or undefined
to Alfred which will probably result in a crash.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried outputting an array of null
s and noticed something pretty interesting. If you're calling your script from a script filter, Alfred just hangs when you output an array of non-objects.
// Example 1: Alfred hangs when this is called from a script filter
alfy.output(['hi', 'hey']);
// Example 2: same for this
alfy.output([null, null, undefined]);
When called from a Run Script action, you just get stringified JSON as your Alfred {query}
:
'{
"items": [
null,
null
]
}'
How does that influence our decision here? Not quite sure. If we use
item = item || {};
that will protect users from example 2 above, but not from example 1. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just throw if it's a non-object. Better to fail loudly than being silently "smart".
Could use: https://github.com/sindresorhus/is-plain-obj
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
index.js
Outdated
return item; | ||
} | ||
|
||
if (isDefined(item.variables)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just write if (item.variables) {
index.js
Outdated
item = wrapArg(item); | ||
} | ||
|
||
if (isDefined(item.mods)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (item.mods) {
index.js
Outdated
}; | ||
|
||
const formatMods = item => { | ||
const copy = Object.assign({}, item); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we need to copy it first. It doesn't really matter in this case if we alter the object or not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
index.js
Outdated
const formatMods = item => { | ||
const copy = Object.assign({}, item); | ||
|
||
Object.keys(item.mods).forEach(modKey => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are more fan of for...of
:)
for (const mod of Object.keys(item.mods)) {
item.mods[mod] = wrapArg(item.mods[mod]);
}
return item;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great! That syntax looks a little nicer to me too. I was wondering myself whether to go with the loop body you suggested.
Done 😄
index.js
Outdated
const wrapArg = item => { | ||
const alfredworkflow = {arg: item.arg, variables: item.variables}; | ||
const arg = JSON.stringify({alfredworkflow}); | ||
const newItem = Object.assign({}, item, {arg}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just put argument on the item
object directly. No need to copy it I guess.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see your reasoning. But I copied the item because I wanted to avoid this kind of scenario:
const unicorn = {
title: 'unicorn',
arg: '🦄',
variables: {
selectedMagicalBeing: true
}
};
alfy.output([unicorn]);
const unicornEmoji = unicorn.arg;
doSomething(unicornEmoji);
// weird behavior. unicornEmoji is now a stringified JSON object
Obviously, you can avoid this sort of problem by calling alfy.output
last. And I'm sure that's how most people use it. I just don't want Alfy users to have to remember to call alfy.output
last.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General rule is to always use Object.assign if you modify a user-specified object. Not doing so can cause subtle bugs.
test/output.js
Outdated
t.notThrows(() => m.output(undefined)); | ||
}); | ||
|
||
const itemWithMod = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put this at the top.
I like calling it |
Yes to me it sounds much better then |
index.js
Outdated
const wrapArg = item => { | ||
const alfredworkflow = {arg: item.arg, variables: item.variables}; | ||
const arg = JSON.stringify({alfredworkflow}); | ||
const newItem = Object.assign({}, item, {arg}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General rule is to always use Object.assign if you modify a user-specified object. Not doing so can cause subtle bugs.
index.js
Outdated
}; | ||
|
||
const formatMods = item => { | ||
const copy = Object.assign({}, item); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
index.js
Outdated
// see https://www.alfredforum.com/topic/9070-how-to-workflowenvironment-variables/ | ||
// for documentation on setting environment variables from Alfred workflows | ||
const format = item => { | ||
if (!isDefined(item)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would just throw if it's a non-object. Better to fail loudly than being silently "smart".
Could use: https://github.com/sindresorhus/is-plain-obj
readme.md
Outdated
@@ -179,6 +179,40 @@ alfy.output([{ | |||
|
|||
<img src="media/screenshot-output.png" width="694"> | |||
|
|||
Using the `variables` property: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A new top-level section called "Environment variables".
readme.md
Outdated
|
||
```js | ||
// After a user selects "Unicorn" or "Rainbow" | ||
process.env.color |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we alias this as alfy.env
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that would be nice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
Yes, let's call it |
@noise-machines ping :) |
Thanks for the ping @SamVerschueren. Will implement the desired changes this week. :) |
@noise-machines Sorry for another ping :p. If you don't find the time to finish this, give me write access to your repository and I might be able to do this for you. Or if you want to do it yourself, feel free :). |
@SamVerschueren, no worries! If I don't get to it in the next few days, I'll give you write access. |
@noise-machines Interested in finishing this up? If not, could you give write-access to me and Sam? |
Okay, I believe all requested changes have been implemented. Thanks for your patience and excellent feedback, @SamVerschueren and @sindresorhus. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the late reply, forgot about this PR. Let's get it merged :).
Have some minor improvements. I also think we should extract the format
(+ all the other extra methods) to a lib/utils.js
file.
index.js
Outdated
// for documentation on setting environment variables from Alfred workflows. | ||
const format = item => { | ||
if (!isPlainObj(item)) { | ||
throw new TypeError(`Expected \`item\` to be a plain object, got ${typeof item}.`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap ${typeof item}
between backticks as well.
index.js
Outdated
}; | ||
|
||
// See https://www.alfredforum.com/topic/9070-how-to-workflowenvironment-variables/ | ||
// for documentation on setting environment variables from Alfred workflows. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put the comment on one line
index.js
Outdated
|
||
alfy.output = items => { | ||
if (!Array.isArray(items)) { | ||
throw new TypeError(`Expected \`items\` to be an Array, got ${typeof items}`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap ${typeof items}
between backticks as well.
readme.md
Outdated
{ | ||
title: 'Unicorn', | ||
arg: '🦄', | ||
variables: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
env
readme.md
Outdated
{ | ||
title: 'Rainbow', | ||
arg: '🌈', | ||
variables: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
env
Hey @SamVerschueren, latest commits should implement those changes. Let me know if there's anything else! |
@@ -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'); |
There was a problem hiding this comment.
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.
@@ -159,6 +165,8 @@ alfy.icon = { | |||
delete: getIcon('ToolbarDeleteIcon') | |||
}; | |||
|
|||
alfy.env = process.env; |
There was a problem hiding this comment.
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?
@@ -0,0 +1,36 @@ | |||
const isPlainObj = require('is-plain-obj'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'use strict';
const alfredworkflow = {arg: item.arg, variables: item.env}; | ||
const arg = JSON.stringify({alfredworkflow}); | ||
const newItem = Object.assign({}, item, {arg}); | ||
delete newItem.env; |
There was a problem hiding this comment.
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.
}; | ||
|
||
// See https://www.alfredforum.com/topic/9070-how-to-workflowenvironment-variables/ for documentation on setting environment variables from Alfred workflows. | ||
module.exports.format = item => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
exports.format
"cache-conf": "^0.3.0", | ||
"clean-stack": "^1.0.0", | ||
"clean-stack": "^1.3.0", |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert
@@ -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. |
There was a problem hiding this comment.
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...
const itemWithMod = { | ||
title: 'unicorn', | ||
arg: '🦄', | ||
env: {fabulous: true}, |
There was a problem hiding this comment.
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
}; | ||
|
||
const m = alfy(); | ||
const hook = cb => { |
There was a problem hiding this comment.
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.
Closing for lack of activity. |
Linter and tests (including new ones) are all green. Also updated the readme. I tried to match the existing line usage and code organization. I'd love feedback. I'm just getting started with open source contributions, and I know there's plenty to learn.