Alfred でオペ作業をちょっと改善する

# はじめに

この記事は[株式会社オープンロジのアドベントカレンダー 2021](https://qiita.com/advent-calendar/2021/openlogi)の6日目の記事です。

Alfred という macOS 用のランチャーを使って、オペ作業をちょっと改善しました。

https://www.alfredapp.com/

# オペとは

オープンロジにおけるオペについては、2020年のアドベントカレンダーで次のように紹介されています。

https://qiita.com/Teloo/items/a72b771576612ea57aa9

>オープンロジの技術開発部では、ビジネスサイドへの技術的な支援やログ監視をする当番を1週間ごとに1〜2名で交代しながら運用しており、この作業を社内では「オペ」と呼んでいます。

再現性のあるオペについては「コピー元チケット」が用意されています。

>すでに過去の実績があってパターン化しているバッチ処理は、プロジェクトは「運用」、トラッカーは「Task」の形で手順が書かれたチケットが用意されていて、各オペ依頼のチケットはこのチケットをコピー元として関連づけしてあるような運用です。

オペの運用方法は2020年当時と比べると少し変わっている部分があるのですが、パターン化している運用を「コピー元」として各オペ依頼のチケットに紐付ける運用は変わっていません。

なので、オペの際には「このオペ依頼チケットに適したコピー元チケットはどれかな？」というちょっとした作業が発生するのですが、現在197件あるコピー元チケットからそれを探し出すのはちょっぴり面倒です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323703/377790fa-8b3b-d6f6-9a0c-3724a6f20929.png)

そこで Alfred を使ってあいまい検索でコピー元チケットに素早くアクセスできるようにしてみました。

期待する動作は次の通りです。

- タイトルの一部をキーワードを入力して検索する
  - <kbd>Enter</kbd> → クリップボードにチケット番号をコピーする
  - <kbd><kbd>Cmd</kbd> + <kbd>Enter</kbd></kbd> → チケットをブラウザで開く

# つくったもの

出来上がったものがこちらです。

https://github.com/inoue441/alfred-cf

※ cf は copied_from の略です。

# 使い方

## Workflow をインポートする

1. https://github.com/inoue441/alfred-cf/releases の `alfred-cf.alfredworkflow` をダウンロードする
1. `alfred-cf.alfredworkflow` をダブルクリックする
1. インポートダイアログで変数 `redmine` を設定する (ブラウザで開くときの URL として利用されます)
   ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323703/608755e0-930b-7191-7388-2d168831c3e2.png)

## `issues.json` を置き換える

1. コピー元チケットの一覧 (`issues.json`) を用意する (解説 > コピー元チケットの一覧を用意する)
1. Workflow を右クリックし、Workflow のディレクトリを開く
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2323703/ff1e23d0-81ed-8ebf-3f5c-d5cebfebfb66.png)
1. Workflow のディレクトリに 1. の `issues.json` をコピーする

# 解説

## コピー元チケットの一覧を用意する

検索対象の一覧を用意します。

Redmine の REST API を使って…、 みたいなことを考えていましたが、CSV としてエクスポートできることに気づいたので、今回はこれを使うことにします。

フォーマットは、公式のヘルプを参考にしながら、次のような JSON にすることにしました。

```json
{
    "items": [
        {
            "number": "<チケット番号>",
            "title": "<チケットタイトル>",
            "subtitle": "<チケットタイトル>",
            "arg": "<チケット番号>",
            "count": 0,
            "modified": "<更新日時>"
        },
    ]
}
```

https://www.alfredapp.com/help/workflows/inputs/script-filter/json/

Redmine からエクスポートした CSV を、下記のスクリプトで JSON に変換します。

```php:format.php
<?php

if (count($argv) < 2) {
    die('CSVファイルを指定してください');
}

$path = $argv[1];
if (!file_exists($path)) {
    die('CSVファイルが見つかりません');
}

$fp = new SplFileObject($path);
$fp->setFlags(SplFileObject::READ_CSV);

$items = [];
foreach ($fp as $row) {
    if ($fp->key() === 0 || $fp->eof()) {
        continue;
    }

    $items[] = [
        'number' => $row[0],
        'title' => sprintf("#%d %s", $row[0], $row[7]),
        'subtitle' => sprintf("#%d %s", $row[0], $row[7]),
        'arg' => $row[0],
        'count' => 0,
        'modified' => null,
    ];
}

echo json_encode(compact('items'), JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT);
```

```console
$ php format.php issues.csv > issues.json
```

## Script Filter

`Inputs > Script Filter` で、`issues.json` を絞り込みます。

```php
<?php

$query = trim($argv[1]);
$query = iconv("UTF-8-MAC", "UTF-8", $query); // 濁点を検索するための対応

$source = file_get_contents('issues.json');
$data = json_decode($source, JSON_OBJECT_AS_ARRAY);

$items = array_values(array_filter($data['items'], function($v) use ($query) {
    if (!strlen($query)) {
        return true;
    }

    return stripos($v['title'], $query) !== false;
}));

echo json_encode(compact('items'));
```

## Run Script

毎回キーワードを打ち込んで検索してもいいですが、最後に呼び出したものが上にきている (MRU = Most Recently Used) と、カーソルだけで選択することもできて便利そうです。
`Actions > Run Script` オブジェクトを使って、実行するごとに JSON を並べ替え、よく使うものが上に集まるようにしてみました。

```php
<?php

date_default_timezone_set('Asia/Tokyo');

$query = $argv[1];

$source = file_get_contents('issues.json');
$data = json_decode($source, JSON_OBJECT_AS_ARRAY);

$found = array_values(array_filter($data['items'], function ($v) use ($query) {
    return (int)$v['number'] === (int)$query;
}));
$found[0]['count'] = (int)$found[0]['count'] + 1;
$found[0]['modified'] = date('Y-m-d H:i:s');

$items = $found;
foreach ($data['items'] as $item) {
    if ((int)$item['number'] === (int)$query) {
        continue;
    }

    $items[] = $item;
}

file_put_contents('issues.json', json_encode(compact('items'), JSON_UNESCAPED_UNICODE|JSON_PRETTY_PRINT));
```

# おわりに

ちょっとした作業改善ではありますが、こういう改善の積み重ねなどで作業を効率化することで、もっと本質的な改善により多くの時間を使えるようにできればいいなーと思います。