Notionでタスク管理する際に、Alfredを使っていい感じにタスク追加できるWorkflowを作りました。
github.com
(Notion DBのプロパティなど自分仕様になっているので、Githubのみの公開)
AlfredのWorkflowを、使い慣れたNode.jsで作るための知見が溜まったので、今回はその辺りをまとめます。
alfy
AlfredのWorkflowのスクリプトを書く際に、標準ではNode.jsで書くことはできません。
「usr/bin/osascript(JavaScript)」というそれっぽい選択肢がありますが、あくまでAppleScriptをJavaScriptっぽく書けるようにしたものなので、わたしたちの知っているNode.jsとは別物です。
そこで、Node.jsを使ってAlfredのWorkflowを書くための便利なライブラリが「alfy」。
alfyが内部的にnodeコマンドへのpathを渡してくれているため、Node.jsで書いたスクリプトをWorkflowのScript上でshell実行することができます。
また、cacheや公開用のコマンドなど、ワークフロー開発に必要な機能を提供してくれています。
使い方
詳しい使い方はGithubのREADMEに書いてあるので、ここではalfyの使い方の簡単な紹介と、ハマったポイントについて書きます。
Workflowのセットアップ
まず、Alfred workflow で 「Blank Workflow」を選択して、空のワークフローを作成します。
このときNameとBundle Idは最低限入力しておきます。
Bundle Idは他のWorkflowと被らないように、ユニークな値であればなんでもOKです。
Bundle Idを入力していないと、あとでこのようなエラーがでます
Code 1: This script must be called from Alfred, $alfred_workflow_cache is missing. Make sure a Bundle ID is set.
右メニューの検索ボックスに「Script」と入力して、「Script Filter」を選択します。
「Script Filter」と「Run Script」はどちらもスクリプトを実行するためのワークフローオブジェクトですが、「Script Filter」は入力時にリアルタイムで実行されるため選択候補の絞り込みができます
キーワードを入力し、言語をNodeをインストールしている場所に合わせて選択、下記のスクリプトを入力します。
./node_modules/.bin/run-node index.js "$1"
後でインストールするalfyがNodeへのpathを通すshellスクリプトを経由してNode.jsスクリプトを実行するため。
$1
はalfredの引数を受け取るための変数です。
左下のフォルダアイコンを選択すると、作成されたワークフローのフォルダが開き、それがプロジェクトディレクトリになります。
alfyのセットアップ
ターミナルで cd
で移動したら、いつもどおりnpmの初期化を行い、alfyをインストールします。
npm init npm install alfy
package.json
に "type": "module"
を追加してESMを使えるようにします。
index.js
を作成し、READMEにあるサンプルコードを実行してみます。
import alfy from 'alfy'; const data = await alfy.fetch('https://jsonplaceholder.typicode.com/posts'); const items = alfy .inputMatches(data, 'title') .map(element => ({ title: element.title, subtitle: element.body, arg: element.id })); alfy.output(items);
Alfredでキーワードを入力するとリスト表示され、更にスペースを開けて文字を入力するとタイトルとマッチした結果が絞り込まれます。
ポイント解説
デバッグ方法
Consoleでログを確認する
Workflow右上の虫アイコンをクリックすると、DevToolsのConsoleのようにログを確認できるビューが表示されます。
ref: Debugger Utility - Alfred Help and Supportテスト
alfyがテストをサポートしているので、vitestなど好みのテスティングツールでテストを書くことができます。
queryとvariables
queryは、次のワークフローオブジェクトに渡される変数です。
上述の arg
というkeyにセットされた値が、queryとして次のワークフローオブジェクトに渡されます。
queryは今->次に渡す一時的な変数で、次のワークフローオブジェクトで上書きされ、複数のワークフローオブジェクト間でそのままでは共有できません。
queryを variables
に格納しておくことで、複数のワークフローオブジェクトから参照できるようになります。
variables
は {var:projectId}
のように参照します。(Node.jsだと process.env.projectId
)
OutputのJSONフォーマット
Node.jsで実装する場合、console.log
で出力されたStringまたは配列がqueryとして次のワークフローオブジェクトに渡されます。(そのため、debug用に複数の console.log
を使うと、それらがすべてqueryとして渡されるので注意が必要です。)
少し複雑なデータを扱いたい場合に、指定のJSONフォーマットを使うことで、より柔軟にデータを扱うことができます。
Script Filter JSON Format
上述で紹介したalfyのサンプルのように、絞り込み結果を表示する場合は以下のようなフォーマットを使います。
console.log(JSon.stringify({ "items": [ { "uid": "desktop", "type": "file", "title": "Desktop", "subtitle": "~/Desktop", "arg": "~/Desktop", // queryとして渡される "autocomplete": "Desktop", "icon": { "type": "fileicon", "path": "~/Desktop" } } ] }))
つまり、alfy.output
は items
というkeyの値となる配列を渡して console.log
で出力するラッパー関数です。
https://github.com/sindresorhus/alfy/blob/fcd33259bd6ce36a997c0b904351374730799066/index.js#L44-L46:embed:lang=javascript
ref: Script Filter JSON Format - Alfred Help and Support
JSON Utility
下記のようなフォーマットを出力することで、変数化も一緒に行うことができます。
console.log( JSON.stringify({ alfredworkflow: { arg: process.argv[2], // = alfy.input, queryとして渡される variables: { hogevar: "hogehogevar", // {var:hogevar}として渡される }, }, }) );
ref: JSON Utility - Alfred Help and Support
alfyのcache機構を使う
alfy.fetch
のオプションに、maxAge
を指定することで、キャッシュの有効期限を設定できます。デフォルトはキャッシュが効きません。
cacheは url
と options
に対して作成した一意のキーで判別されています。
https://github.com/sindresorhus/alfy/blob/fcd33259bd6ce36a997c0b904351374730799066/index.js#L129:embed:lang=javascript
環境変数の設定
環境変数は、右上のXアイコンをクリックして表示される Environment Variables
から設定します。
「Don't export」にチェックを入れると、AlfredからExportしたワークフローファイルにはkeyのみしか引き継がれません。
ただし、alfyを使って npm module として公開する場合、チェックをいれていても info.plist
に値が露出しているため、秘匿情報が含まれる場合は注意が必要です。
まとめ
alfyを使うことで、Node.jsでAlfredのWorkflowを書くことができるようになりました。ただ、もともとAlfredのWorkflowの仕組みをあまり知らなかったので、どこからどこまでalfyの機能なのか把握するのに若干時間がかかりました。
まだWorkflow作成の全容は把握できてないのでもっといいやり方があるかもしれませんが、とりあえず自分でサクッと作れるようになったので、長年使ってきたAlfredがまた一段と活躍しそうです。汎用化できそうなものができたらまた公開したいと思います。