【Alfred Workflow】Script Filterで自作コマンドを開発する

せっかくAlfred Powerpackに入っているのでもっと使いこなしたいと思い、Workflowを使ってツールを作ってみた。
作ったのはこんな感じのモールス信号解読機。趣味でパズルを解いたりしていると、ふとしたときに意外と出てくるので、爆速で立ち上がるAlfredで解読したいと思っていた。

Alfred Workflowとは

Alfred Workflowを使うと、Alfredを拡張し、独自のキーワードで様々な機能を開発することができる。ただし有料ライセンスであるPowerpackを購入していることが条件。
全体の流れはGUIで作成するが、ビジネスロジックの部分はスクリプトで書くことができる。対応言語もBash, PHP, Ruby, Pythonなどいろいろある。

Script Filterを設定する

今回のツールでは、

  • モールス信号を入力している間に結果がリアルタイムで下に表示され、更新されていく
  • Enterキーを押すと結果がクリップボードにコピーされる

という挙動にしたかったので、Script Filterを使用した。
Script Filterはもともとは検索機能のために用意されているもので、スクリプトの実行中にAlfredに結果を表示できる。また、どのような書式で結果を表示するかもカスタマイズできる。

www.alfredapp.com

Script Filterを設定する

Alfred Preferenceの画面からWorkflowsを選ぶと、新しいWorkflowを開発できる。ちなみにAlfredの入力画面で alfred と入力すれば、Alfred Preferenceに飛べる。
右クリックで Inputs以下にある Script Filter を選択すると、新しいScript Filterを作成できる。また、最終結果をクリップボードに渡したいので、Outputとして Copy to Clipboardを選び、Script Filterから線をドラッグしてつないでおく。

f:id:Udomomo:20220206110325p:plain

Script Filterを作成すると、以下のような設定画面になる。 Keyword の欄には、このworkflowを開始するためにAlfred上で入力するキーワードを指定する。引数はqueryとargvのどちらでも良いが、エスケープの心配がいらない点やパフォーマンス面から、Alfredではargvを推奨している。

f:id:Udomomo:20220206110440p:plain

Run Behaviourからは、入力中のスクリプトの挙動を設定できる。文字をどんどん入力していったら前の段階での結果はいらなくなるので、Queue ModeTerminate previous script にする。また、結果のリアルタイム表示といっても、入力が一段落した時点でスクリプトを実行すれば十分なので、 Queue DelayAutomatic delay after last character typed にする。

f:id:Udomomo:20220206111256p:plain

スクリプトを作成する

今回はPythonで作成した。日本語と英語の解読ができれば良かったので、モールス信号の辞書をそのまま貼ったのだが、その部分は長いので省略している。

# coding: utf-8

import json
import sys

MORSE_CODE_DICT_EN = {
  'a': '.-', 'b': '-...', 'c':'-.-.', 'd':'-..', 'e':'.', 'f':'..-.', 
  ...
}

MORSE_CODE_DICT_JA = {
  'あ': '--.--', 'い': '.-', 'う': '..-', 'え': '-.---', 'お': '.-...',
 ...
}

def decrypt(lang, input):
  if lang == 'ja':
    return _morse_to_letters(input, MORSE_CODE_DICT_JA)
  elif lang == 'en':
    return _morse_to_letters(input, MORSE_CODE_DICT_EN)
  else:
    return 'Invalid argument. Usage: <lang (`ja` or `en`)> <morse code (`.` or `-`, split by space)>)'
  

def _morse_to_letters(input, morse_dict):
  if not input:
    return 'No morse code input.'
  reversed_dict = {value:key for key,value in morse_dict.items()}
  result = ''
  for m in input:
    result += reversed_dict.get(m, '#')
  return result

if __name__ == '__main__':
  args = sys.argv[1].split()
  lang, input = args[0], args[1:]
  result = {
    'items': [{
      'uid': 'morse',
      'title': decrypt(lang, input),
      'arg': decrypt(lang, input)
    }]
  }
  sys.stdout.write(json.dumps(result, ensure_ascii=False))

注意すべきポイントは3つある。まず、通常のPythonスクリプトsys.argvとは異なり、Script Filterではキーワードの後に入力した引数全てがsys.argv[1] に入る。例えば morse ja .-- --.-- と入力する場合、ja .-- --.--sys.argv[1]の値になる。最終的なargsの数を予測できないので当然といえば当然だが、自分はここでかなりハマった。
2つ目は、Alfred WorkflowはPython 2のみに対応していること。これはWorkflowがMacのビルトイン環境のみで動くことを重視しているためらしい。今回の場合、コード内でひらがなを使っているため、冒頭に # coding: utf-8 をつけないとエラーになった。
最後に、Script FilterからAlfredに結果を渡すためには、専用のJSONまたはXMLフォーマットに従う必要がある。JSONフォーマットは以下のページに記載されている。

www.alfredapp.com

複雑なWorkflowになると、専用のライブラリ(例: alfred-workflow )を使うことも多いが、今回は単純なスクリプトなので手動でJSONを作った。

{
    'items': [{
       'uid': 'morse',
       'title': decrypt(lang, input),
       'arg': decrypt(lang, input)
    }]
}

uidはAlfredが結果のitemを識別するためのID(必須ではない)。titleの部分に解読結果を載せている。また、 args に指定された値は、Enterキーを押した時点でOutputに渡される。今回の場合、argsにも解読結果を指定することで、最終的な結果をクリップボードに渡すことができる。
この他にも任意のフィールドがたくさんあり、アイコンの指定などもできる。

これでいつものAlfredウィンドウから、morseコマンドが使えるようになった。
なお、Alfred Workflowにはデバッグツールも用意されているので、Script Filterが思うように動かない場合はログを確認してみよう。

www.alfredapp.com