kokh log

yumikokhの開発日記

Node.jsでAlfredのWorkflowをつくった

Notionでタスク管理する際に、Alfredを使っていい感じにタスク追加できるWorkflowを作りました。

github.com
(Notion DBのプロパティなど自分仕様になっているので、Githubのみの公開)

AlfredのWorkflowを、使い慣れたNode.jsで作るための知見が溜まったので、今回はその辺りをまとめます。

alfy

AlfredのWorkflowのスクリプトを書く際に、標準ではNode.jsで書くことはできません。
「usr/bin/osascript(JavaScript)」というそれっぽい選択肢がありますが、あくまでAppleScriptJavaScriptっぽく書けるようにしたものなので、わたしたちの知っているNode.jsとは別物です。
そこで、Node.jsを使ってAlfredのWorkflowを書くための便利なライブラリが「alfy」。

github.com

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でキーワードを入力するとリスト表示され、更にスペースを開けて文字を入力するとタイトルとマッチした結果が絞り込まれます。

ポイント解説

デバッグ方法

  1. Consoleでログを確認する
    Workflow右上の虫アイコンをクリックすると、DevToolsのConsoleのようにログを確認できるビューが表示されます。

    ref: Debugger Utility - Alfred Help and Support

  2. テスト
    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.outputitems という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 を指定することで、キャッシュの有効期限を設定できます。デフォルトはキャッシュが効きません。

https://github.com/yumikokh/alfred-notion-task/blob/e759af05d44a806efea6066e847e5b3697a04b1b/src/project.js#L42:embed:lang=javascript

cacheは urloptions に対して作成した一意のキーで判別されています。
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がまた一段と活躍しそうです。汎用化できそうなものができたらまた公開したいと思います。