- はじめに
- Alfred Workflowとは?
- Alfred Workflowの仕組み
- Alfred WorkflowとGo
- github.com/deanishe/awgo
- チュートリアル
- macOSのセキュリティ
- deanishe/awgoの微妙なところ
- 紹介しきれていない機能
- まとめ
はじめに
これはGo3 Advent Calendar 2019,7日目の記事です.
Alfred Workflowとは?
macOS向けの多機能ランチャーであるAlfredの自動化のためのプラットフォームです.Alfredとは,簡単に言ってしまうとmacOSのSpotlightの機能強化版です.
過去に以下のようなものを作りました.
最後の一つはAlfredのUIを一切使っていないので少し毛色が違いますが,上2つの様な,
- 何らかのトリガーとなるコマンドに対して
- 選択肢を提示し
- ユーザの選択によって何かアクションを起こす
というものを作りたくなったときに便利に使えます.なお,Alfred本体は無料なのですが,それとは別にPowerPackと呼ばれる拡張パッケージを購入することで使えるようになります.現在のメジャーバージョンでのみ使えるSingle License(25ユーロ),永久ライセンスのMega Supporter(45ユーロ)という二つの形態のライセンスがあるのでお好きな方を購入してください.
Alfred Workflowの仕組み
様々な形態のWorkflowがありますが,基本的には用意されているコンポーネントをGUIで繋いでいき,あるコンポーネントから別のコンポーネントへ,文字列を受け渡して様々なアクションを起こします.
自作のスクリプト/バイナリを使ってAlfredのUIを使用したい場合,Script Filterというコンポーネント内でそのスクリプト/バイナリを実行し,所定の形式を持つJSON*1を標準出力へprintします.そのJSONをAlfredが読み取り,良い感じにUIを表示して,選択されたオブジェクト(の中の,次のコンポーネントへ渡す引数となる文字列)を接続されているコンポーネントへ受け渡します.
実行できるアクションには様々な物があり,
- ブラウザで検索
- URLを開く
- 任意のアプリケーションで開く
- 任意のスクリプトを実行する
などなど,多数のアクションが用意されています*2.こうしたアクション以外にも,クリップボードへコピーしたり,通知をだしたりも出来ます.便利ですね.
Alfred WorkflowとGo
Alfred Workflowで動かすアプリケーションをGoで書くというアプローチは,数年前から行われています.
これらのサイトでも述べられていますが,
- シングルバイナリになるので簡単に配布して簡単に使える
という部分が最も大きいです.全てをシェルスクリプトで書ける猛者ならまだしも,どんなモジュールが入っているか分からないPythonやPerlで頑張るよりも簡単かつ安全かと思います.
github.com/deanishe/awgo
今までは自分の手で書いたstructをjson.Marshal()
していましたが,便利なライブラリが開発されていました.
Alfred3および4用のライブラリで,どちらでも使うことが出来ます*3.
また,表示するアイテムの出力だけでなく,様々な機能のサポートが含まれています.長くなるので今回は紹介しませんが,一通り調べてみたので機会があればそれについても記事を書きたいと思っています.
チュートリアル
以下の環境で実行しています.
あまり良い題材を思い浮かばなかったので,今回はこのAlfred Workflowで使った依存パッケージのライセンスを閲覧できるAlfred Workflowを作ってみようと思います.ビルドしたバイナリを配布する場合,使用した各モジュールのライセンスに則って著作権表示等が必要となります*4.
リポジトリはこちら
1. 空のWorkflowを作る
左下の+
からBlank Workflow
を選択すると新しいWorkflowを作成するウィンドウが開きます.
Bundle idはとにかく一意な値であれば良いので,自分の持っているドメイン等があればそこから適当な値を自分で考えれば良いと思います.ここで記述した内容は後からでも変更可能なので,あまり迷う必要はありません.
2. 実行するアクションを並べる
依存パッケージとライセンスの取得には以下のパッケージを利用させて頂きます.
これは出力方式としてJSONを選択することができ,その場合各モジュールの名前とLICENSEファイルの中身を取得できます.閲覧のためにはその中身を一旦ファイルに出力する必要があるので,以下の様な構成とします.
- モジュール名とライセンスファイルの中身から表示するアイテムを作成するスクリプトを実行
- 選択したモジュールのライセンスが文字列で渡されるので,これをWorkflow内のテンポラリディレクトリに出力
- そのファイルを規定のプログラムで開く
加えて,Modifierを使ったときの例も示したいので,1の後にCommand + Enterを押すことでそのモジュールのURLをブラウザで開く,というタスクも追加します.
各コンポーネントは,右クリックして出てきたメニューから選択して作成できます.各コンポーネント間はドラッグで紐付けることが出来ます.虫のマークはデバッグコンポーネントで,そのコンポーネント間でやりとりされる引数や変数を見ることが出来ます.
各コンポーネントの設定はそのコンポーネントをダブルクリックで開けます.
Open File,Open URLコンポーネントはデフォルトのままです.Command+Enterでアイテムを選択したときにOpen URLへ動作を分岐させる方法は,コンポーネント間の接続(エッジ)をダブルクリックすると出てくるメニューから,使用するModifierにチェックを入れるだけです.他のModifierもあるので,更に様々な操作を行うことが出来ます.
3. info.plistを手に入れる
次に,左にあるWorkflow一覧から,このWorkflowを選び,オプションメニューからFinderで開くを選択します.
info.plistというファイルがあると思うので,それを次に作るGoのプロジェクト内にコピーします.このファイルを単体で編集するのは骨が折れるので,基本的にコンポーネントの操作等はAlfredのUI上で行い,それをその都度Goのプロジェクトへコピーするようにしています.
4. Goでプログラムを書く
まずはプロジェクトを作り,deanishe/awgoをインストールします.
$ mkdir sample-alfred-workflow $ cd sample-alfred-workflow $ go mod init $ go get github.com/deanishe/awgo
Songmu/gocreditsが吐くJSONは以下の様なフォーマットなのでそれに合わせてstructを作ります.
{ "Licenses": [ { "Name": "モジュール名", "URL": "URL", "FilePath": "ファイルパス", "Content": "LICENSEの中身" } ] }
license.go
として以下の内容を記述します.特に難しいところは無いはずです.
package main import ( "encoding/json" "fmt" "io/ioutil" ) // 各モジュールごとのLicenseを表すstruct type license struct { Name string URL string FilePath string Content string } // モジュールごとのライセンスを全て内包するstruct type licenseJson struct { Licenses []license } // 与えられたパス名からライセンスを読み取り,ライセンスの一覧を返す func getLicenses(path string) ([]license, error) { contents, err := ioutil.ReadFile("./credits.json") if err != nil { return nil, fmt.Errorf("%s does not exists.\n", path) } var licenses licenseJson if err = json.Unmarshal(contents, &licenses); err != nil { return nil, fmt.Errorf("Failed to parse %s.\n", path) } return licenses.Licenses, nil }
ようやくdeanishe/awgoを使ってメインのロジックを書いていきます.とは言っても,この程度のAlfred Workflowならば非常に簡単になります.
package main import ( "fmt" "os" "strings" aw "github.com/deanishe/awgo" ) var ( // このプロジェクト全体で参照するaw.Workflow wf *aw.Workflow ) func init() { // 初期化時にインスタンスを生成する wf = aw.New() } func run() { licenses, err := getLicenses("./credits.json") if err != nil { // wf.Fatal~~は,エラーメッセージを出力して終了させる wf.FatalError(err) } for _, c := range licenses { // ループを回して一つずつItemを作っていく // Itemの持つ各メソッドはItem自身を返すのでメソッドチェーンで書ける item := wf.NewItem(c.Name). // Argは選択されたときに次のコンポーネントへ渡す引数となる文字列 Arg(c.Content). Subtitle(strings.Split(c.Content, "\n")[0]). // Valid(true)としないと選択できないので注意 Valid(true) // そのアイテムがCommand+Enterで選択されたときの動作を変更する item.Cmd(). Arg(c.URL). Subtitle(fmt.Sprintf("Open %s in browser", c.URL)). Valid(true) } // 与えられた文字列でアイテムをフィルタリングする args := os.Args if len(args) > 1 { // https://godoc.org/github.com/deanishe/awgo/fuzzy wf.Filter(args[1]) } // 最終的に表示すべきアイテムが無かったときに表示するエラー文 wf.WarnEmpty("No credits were found.", "Try different query.") // 標準出力へ最終的なJSONをプリントする wf.SendFeedback() } func main() { // 内部でpanic等をうまくハンドリングしてくれる wf.Run(run) }
wf.NewItem()
を呼び出した時点で表示するアイテム一覧の中にそのアイテムが追加されるため,どこかにappendしたりする操作は不要です.
6. パッケージングする
ググっているとAlfredのUI上からExportしないとAlfred Workflowとしては動作しないというように言われていますが,実は.alfredworkflow
というファイルの実態は単なるzipファイルです.そのため,単に必要なファイルを全てzipでアーカイブにして拡張子を変えるだけで済みます.今回は以下の様なMakefileを用意しました.
SHELL := /bin/bash PLIST=info.plist CREDITS=credits.json EXEC_BIN=sample-alfred-workflow DIST_FILE=sample.alfredworkflow GO_SRCS=$(shell find -f . \( -name \*.go \)) all: $(DIST_FILE) $(CREDITS): go.sum gocredits -json . > $(CREDITS) $(EXEC_BIN): $(GO_SRCS) go build -o $(EXEC_BIN) . $(DIST_FILE): $(EXEC_BIN) $(CREDITS) $(PLIST) zip -r $(DIST_FILE) $(PLIST) $(CREDITS) $(EXEC_BIN)
単にmake
とするだけでsample.alfredworkflow
が生成されます.
$ make # 関連付けされているのでAlfredが開いてインストールできるはず $ open sample.alfredworkflow
7. CI/CDする
GitHub Actionsが正式リリースになっているのでこれを使いましょう..github/workflows/release.yaml
として保存します.
name: Release on: push: tags: - "v*" jobs: release: runs-on: macos-latest steps: - name: Checkout source codes uses: actions/checkout@v1 with: fetch-depth: 1 - name: Setup Go environment uses: actions/setup-go@v1 with: go-version: 1.13 - name: Restore cache if available uses: actions/cache@v1 id: cache with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - name: download modules if: steps.cache.outputs.cache-hit != 'true' run: go mod download - name: Build run: make - name: Create new release id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false prerelease: false - name: upload release asset id: upload-release-asset uses: actions/upload-release-asset@v1.0.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./sample.alfredworkflow asset_name: sample.alfredworkflow asset_content_type: application/zip
これでタグを付けると以下の様にリリースされます🎉
macOSのセキュリティ
Catalinaでは,署名されていない,インターネットからダウンロードしたバイナリの実行をデフォルトでブロックします.そのため,この方法でAlfred WorkflowをGitHub Releasesからダウンロードすると以下の様に警告を吐かれます.
システム環境設定 > セキュリティとプライバシーから実行を許可して再度実行するとまた警告が出ます.
ここで「開く」を選択すると,ようやく次からAlfred Workflowを使用できます.
別にマルウェアなどは仕込んでいないので安心してください……
deanishe/awgoの微妙なところ
- 何もかもstructなのでテストしづらい
aw.Workflow
があらゆるビジネスロジックを担っている巨大structなので剥がしづらい- 小さいInterfaceを自分で定義していくのがいいかも
- グローバルな
aw.Workflow
を使い回す設計なのでテストしづらいwf.Run(fn func())
の中で実行することを想定しているので,この設計以外で使えない
wf.SendFeedback()
がDIの機構を持たないので出力をテストしづらい- 直接stdoutに書き込んでいて外側からインジェクション出来ない
wf.Feedback.MarshalJSON()
で生成されるJSONをテストするだけで凌ぐ…?
wf.NewItem()
がgoroutine safeじゃない- 内部的には全てのアイテムは
wf.Feedback
が管理しており,wf.NewItem()
はwf.Feedback.NewItem()
を呼んでいる - FeedbackのItemsに単に作ったItemをappendしており,goroutineで普通に叩くと壊れる https://github.com/deanishe/awgo/blob/master/feedback.go#L421
- 普通に
aw.NewItem()
みたいなのとwf.AddItem()
みたいなのがあれば十分だったと思う
- 内部的には全てのアイテムは
wf.NewItem()
が返すItemがデフォルトでvalid=falseになっているvalid=false
なオブジェクトはUIに表示できるが選択できない(エラーメッセージとかを表示するのに使う)- デフォルトがfalseなのはAlfredの文脈的には直感に反する
- 作者の意見:github.com
紹介しきれていない機能
リポジトリに上がっている参考実装がかなりためになります.https://github.com/deanishe/awgo/tree/master/_examples
- そのWorkflow独自の変数をAlfredのUI上からも,ライブラリ経由でも変更・取得できる https://godoc.org/github.com/deanishe/awgo#Config
- macOSのKeyChainにも対応 https://godoc.org/github.com/deanishe/awgo/keychain
- シンプルなキャッシュ機能 https://godoc.org/github.com/deanishe/awgo#example-Cache
- セルフアップデートAPIの利用 https://godoc.org/github.com/deanishe/awgo/update#example-GitHub
などなど
まとめ
- Alfred WorkflowをGoで書くならdeanishe/awgoは機能が豊富で便利
- GitHub ActionsでCI/CDも出来る
- macOS Catalinaのセキュリティ機能はかなりおせっかい
- deanishe/awgoは癖が強いのでテストはちょっと色々考える必要がある
皆さんの面白いAlfred Workflowをお待ちしています.