エンジニア向けのタスク管理ツールを作ってみた

# はじめに

## 思い
エンジニアとして毎日を創造的に、でものびのびと過ごしたいと思っています。「管理」という言葉は嫌いです。しかし、そうはいっても、やらなければならない雑務やら用事やらは多々あり、それらを頭だけで処理するのは厳しいものがあります。そもそも貴重な頭をそんなことに使いたくない。

そこでタスク管理に頼るのですが、従来の GUI ベースのツールにはいまいち馴染めませんでした。「ならば自分で作ってみよう」と思い立ち、そこそこ形になったと思うので今回まとめてみます。

## 本記事が含む内容
- タスク管理ツール Tritask の概要と成果物リンク
- 「（特にエンジニア向けの）タスク管理」に対するポエム
- Tritask の実装に関する技術的な話

## 対象読者
- 「エンジニア向けのタスク管理」に対する一つの解釈について知りたい方
- 「エンジニア向けのタスク管理」についてアイデアや刺激が欲しい方
- 既存のタスク管理ツールに不満を抱いている方
- タスク管理について思い悩んでいる方
- タスク管理ツールやその開発について興味がある方

# 実物
**Tritask** というツールを作ってみました。

Tri は 3 を意味し、タスクを「今日」「明日」「昨日以前」の三種類に分類するというコンセプトです。もっと言うと、

- 今日のタスクにだけ集中する
- 明日以降のタスクは明日やればいい
- 昨日以前のタスクは適宜見返すなり集計するなり

という具合です。

## デモ
![tritask_sta_demo](https://user-images.githubusercontent.com/23325839/36819300-517d1a7a-1d2c-11e8-92bb-7cbcf4fd2cbd.gif)

テキストエディタ（私が愛用する秀丸エディタですが）上でサクサク動かせる感じのツールです。その気になれば他のエディタ用も開発できると思います。

## 成果物へのリンク
GitHub に置いてます。

- [タスク管理メソッド Tritask の概要と仕様](https://github.com/tritask/tritask-spec)
- [実装例(Tritask-sta)](https://github.com/tritask/tritask-sta)
- 2020/02/16 追記 [tritask/tritask-vscode: VSCode 版 Tritask](https://github.com/tritask/tritask-vscode) VSCode 版もあります

## 体系
ちらっと「実装」という言葉が出てきましたが、Tritask の体系は少し ~~カッコつけてます~~ オープンソースっぽくしています。具体的には **「仕様」と「実装」に分けて** みました。

- Tritask はタスク管理 **メソッド** (仕様)
- Tritask-sta は、Tritask を実装した **一つのツール** (実装)

実装としては今は Tritask-sta しかありませんが、仕様に従えば Vim 用とか Atom 用も開発できると思います。

# タスク管理とは
いきなりですが、「タスク管理」という言葉は非常に広域なので、本記事が言及する「タスク管理」についてまとめておきます。

エンジニア像についても言及しており、若干、いやかなり偏りの強い内容かもしれません（一応ポエムタグも付けときました）。

## タスクとは「個人のタスク」のこと
- **個人のタスク** を **個人で** 管理する
- 人に見せることは考えていない
  - 第三者向けの表現に時間を割く必要はない

## タスク管理に望むこと
- 見逃さない（ **存在を忘れて実行し損ねない** ）こと
- 今日やらなくてもいいタスクを先送りにできること
- 定期タスクは必要な時だけ見せること
  - 例: 日曜日に一度やるタスクは日曜日に見せてくれればいい(月-土は見せなくていい)
- あまり厳密/煩雑な使い方を強要しないこと( **ある程度いいかげんに使えること** )

## タスク管理はバリバリ手を動かす
タスク管理とは、多数のタスクを書き換えたり並び替えたりすることを繰り返す作業である。言い換えると **コーディングみたいにバリバリ手を動かす作業** とも言える。（したがって快適にエディットできねばならない）

## モバイル利用は考えない
エンジニアはオレオレカスタマイズした PC に張り付いて作業する生き物なので、 **タスク管理ツールも PC で使うことだけ考えればいい**。

モバイル利用を考慮するがゆえに PC 利用の利便性が損なわれてしまっては本末転倒。

## タイムトラッキング(時間管理)は考えない
見積もりとか工数とか、そういうのには縛られたくない。それは個人ではなくチームのタスク管理でやること。

個人のタスク管理ではもっとのびのびと過ごしたい。

# Tritask-sta について
今回紹介した実装例(ツール）である [Tritask-sta](https://github.com/tritask/tritask-sta) について、簡単に取り上げてみます。

## コンセプトについて 5 行で
- タスク管理メソッド
- タスクを「昨日以前にやったもの」「今日やるもの・やったもの」「明日以降やるもの」の三種類に分け、今日やるものに集中する
- 全タスクはソート（昇順並び替え）することで並び替える
- データは Plain Text
- オープンソース

## 使い方について 5 行で
- テキストエディタでガシガシ編集する
- 一行に一タスクを書く
- タスクの開始 = 開始時刻を記入する、タスクの終了 = 終了時刻を記入する
- 昇順ソートしたら「時系列順」とか「終了タスクずらり → 開始タスクずらり」とか、上手い具合に見やすく並ぶ（並ぶようにフォーマットを練ってある）
- 全部手で書いてもいいけど、ちょっとしんどい。エディタのマクロ/プラグイン/外部呼び出し等を使って省力化するのがオススメ

## フォーマットについて
一行分のフォーマットはこんな感じ:

```
(M) (YYYY/MM/DD) (DOW) (HH:MM) (HH:MM) (DESCRIPTION)
```

日本語で書くとこんな感じ:


```
(ソート制御文字) (実行日付) (同 曜日) (開始時間) (終了時間) (タスク名とかその他備考とか)
```

## もっと詳しい資料は？
この辺にまとめてみます。

- [Tritask ファイルフォーマット](https://github.com/tritask/tritask-spec/blob/master/format_trita.md)
- [Tritask-sta 仕様](https://github.com/tritask/tritask-sta/blob/master/specification.md)
- [スタートアップガイド的なもの](https://github.com/tritask/tritask-spec/blob/master/startup_trita.md)

# 技術的な話
Qiita なので技術ネタも……ということで、Tritask-sta の実装について雑多に書いてみました。

といっても秀丸エディタと Python を使っているだけで、特に目新しい技術や手法などはありません。Tritask の具体的なイメージは湧くかもしれません。

## 登場人物

| 名前            | 内容 |
| --------------- | ---- |
| `XXXX.trita`    | データファイル |
| `tritask.mac`   | 秀丸エディタから呼び出すマクロ |
| `helper.py`     | マクロから適宜呼び出すスクリプト(マクロ単体で実現できない処理を担当) |
| `trita.hilight` | trita ファイル用の強調定義ファイル |

## 呼び出しの流れ
- XXXX.trita を秀丸エディタで開く
- Tritask を使いたい時はマクロ `tritask.mac` を呼び出す
  - 呼び出し方は適当に設定しておく。メニュー or ショートカットキー or ツールバーボタン etc...

**マクロを呼び出すとメニューが表示される** ので、選択したい操作を選ぶ……という形です。

メニューにはアクセラレータが設定されているので、ショートカットキーと併用すればキー二回で各操作を呼べます。ちなみに私は Alt + A を設定してます。タスク追加(Add)の場合、 `Alt + A → A` でいけます。左手だけで素早く呼び出せるよう設計しています。

## データファイル用拡張子を用意した理由
Ans: 強調表示したかったから。

最初は「 .txt でも .md でも何でもいい」としていたのですが、それだと秀丸エディタで強調表示できませんし、他のエディタ用実装をつくる際も不便だと思ったので、専用の拡張子を用意することにしました。

## tritask.mac(エディタのマクロ機能) と helper.py(外部スクリプト) の役割
基本的にエディタのマクロ機能のみで完結させるのが望ましいですが、 **秀丸エディタマクロでは出来ない処理があったので Python に頼りました**。具体的には以下です。

- .trita ファイルのソート
- 日付処理全般
  - 例: `2018/03/19 Mon` を 3 日進めて `2018/03/22 Thu` にする

ちなみに Python なのは私の趣味です。

## tritask.mac(マクロファイル) の実装について
秀丸エディタマクロの話が続きます。

※秀丸エディタマクロ用のシンタックスハイライトが無いので読み辛いですが :sweat:

### メニューの表示位置
カーソル位置に出すかマウス位置に出すか、を少し工夫してあります。

```
#use_mouse_pos = true;
if(iskeydown(0x10)){
	#use_mouse_pos = false;
}
if(iskeydown(0x11)){
	#use_mouse_pos = false;
}
if(iskeydown(0x12)){
	#use_mouse_pos = false;
}
```

- もし修飾キーが押されていたら → キー割り当て経由からの起動なので **カーソル位置** に出す
- 押されていなかったら → メニューあるいはツールバーから（マウスから）起動しているので **マウス位置** に出す

### メニューの実現
メニューは秀丸マクロの `menuarray` 文を使っています。

これは指定した配列をベースにしたメニューを作る命令です。「メニュー項目データを配列で作って、それを渡しなさい」という感じです。渡してあげると、あとは秀丸エディタ側でメニューを表示してます。

コードでは以下のようにしています。

まず各項目の ID（どの項目が選択されたかは ID で区別されます） を、重複しないように定義していきますが、面倒くさいのでインクリメントコードのコピペで済ませました。` = 1`、` = 2`、という風に書くよりは楽だと思います。

```
#cnt=0;
#I_ADD      = #cnt; #cnt=#cnt+1;
#I_ADDINBO  = #cnt; #cnt=#cnt+1;
...
#I_EDIT_ME  = #cnt;
```

続いてメニュー項目データの定義です。`$items` 配列に項目データを詰めていきます。詰める先の添字は、上記で定義した ID と対応付けます。あと、項目にはアクセラレータも書いておきます。

アクセラレータとは `(&A)` と書くと、表示上は「Aにアンダーラインが入った」ように見え、これは Aキーを押して選べるようになります。Windows API が提供するメニューの機能です。

```
// [[[ menu item start
#idx=#I_ADD; $items[#idx]      = "(&A)Add Task";
#idx=#I_ADDINBO; $items[#idx]  = "(&X)Add Inbox";
...
#idx=#I_EDIT_ME; $items[#idx]  = "(&P)Programming this macro";
#idx=#idx+1; #maxidx = #idx;
```

最後にメニューをつくります。`menuarray` 文はカーソル位置に、`mousemenuarray` 文はマウスカーソル位置に表示する命令です。

```
if(#use_mouse_pos==true){
	mousemenuarray $items, #maxidx;
}else{
	menuarray $items, #maxidx;
}
```

### タスクの CRUD
可能な限り秀丸エディタマクロ上で完結させています。

以下命令あたりを組み合わせて上手いことやってます。

- `golinetop` : 行頭に飛ぶ
- `insertline` : 空行を挿入する
- `insertreturn` : 改行する
- `delete` : 削除（deleteキーを1回押した挙動）
- `right N` : N文字分右へ（RightキーをN回押した挙動）

ただし、「この部分からこの部分まで消したい」的な処理が結構必要だったので、フォーマット部分を以下のように定数化して、（delete や right の回数などを）定数ベースで指定しています。

```
// 012345678901234567890123456789
//                              INBOX
// M 2017/07/11 Thu  9:52 10:33 YESTERDAY
// M 2017/07/12 Wed             TODAY 未着手
// M 2017/07/12 Wed  9:52       TODAY 作業中
// M 2017/07/12 Wed  9:52 10:33 TODAY 終了
// M 2017/07/13 Thu             TOMORROW
#POS_SORTMARK    = 0;
#POS_DATE        = 2;
#POS_DOW         = 13;
#POS_STARTTIME   = 17;
...
...
```

### 開始中のタスクにジャンプする
**検索機能で「開始中を表す文字列表現」を検索させる** ことで実現してみました。

コード部分はこんな感じです。

```
JUMP_TO_START:
if(#si==#I_JUMP_STA){
	// 検索設定が上書きされるのを防ぐため, 
	// 現在の設定を保持してから検索を行い, 終わった後で復旧する.
	#oldsettings = searchoption;
	$oldgrepee   = grepfilebuffer;
	$oldquery = ""; // 元々検索語は保持されてないのでテキトーに空文字で.

	$query = "[0-9]{2}\\:[0-9]{2}( ){7}";
	searchdown2 $query, regular, loop;

	setsearch $oldquery, #oldsettings;
	setgrepfile $oldgrepee;

	endmacro;
}
```

開始中タスクは `13:25      ` みたいに開始時刻のみ記入してあるので、それを正規表現で探してます。

その他 `searchdown2` やら `searchoption` やら検索系の命令については、マクロヘルプを読んで格闘しながら何とか仕上げました。

### 関係無いファイルを壊さないためのガード
Tritask-sta のソート機能は「ファイル内容を昇順ソートして上書き保存する」という **破壊的な** 機能です。うっかり関係のないファイルに適用しちゃうと大変なことになりますので、ガードをかけています。

```
if_not_trita_then_end:
	if(filetype!=".trita"){
		endmacro;
	}
return 0;
```

.trita ファイル以外なら処理中止、という風にしています。

### helper.py の呼び出し方
基本的には `run` 文を使ってます。

```
	$program = $USING_PYTHON_BIN + " " + currentmacrodirectory + "\\helper.py";
	$args = "-i " + $OPENING_FILE_FULLPATH + " " + $yargs + " -d " + $day_str + " --walk";
  run $program + " " + $args;
```

`$USING_PYTHON_BIN` は Python スクリを実行するインタープリタを指定していて、、普通は `python` ですが、Tritask では  `pythonw` を使ってます。 `python` でないのは、`python` だと **実行する度に黒い画面が出ちゃって煩わしい** からです。

`currentmacrodirectory` は、このファイル(tritask.mac)のあるディレクトリを表す定数です。helper.py へのパスを相対パスで指定するために付けてます。こうしておくと、どのパスにある trita ファイルを使っても問題なく helper.py にアクセスできます。パス指定絡みはハマりやすいので、こうやって明示的に指定するのが私なりのルールです。

### helper.py 側で trita ファイルが更新された後、それを検出する方法
基本的に秀丸エディタ側で自動検出してくれるので処理は不要……だと思っていたのですが、 helper.py を `pythonw` で呼び出した場合はそうもいかないようなので、明示的にリロードを行わせています。

コードはこの部分です。

```
smart_reload:
	if(#use_explicit_reload==true){
		title "Reloading...";
		reopen;
		title 0;
	}
	return 0;
```

`reopen` 文で「今開いてるファイルを再読み込みしろ」と命令してます。これにより、ソートやら何やらで内容を変更しても、秀丸エディタ側で反映できるようになります（ **明示的に再読み込みして反映しているように見せています**）。

## helper.py(Pythonで書いたヘルパースクリプト) の実装について
Python の話が続きます。

### インターフェース
コマンドラインツールという形で実装しました。trita ファイルパスを受け取って動作します。ただのコマンドなのでテストも割と容易です。

引数解析には `argparse` を使ってます。コードは以下です。

```python
def parse_arguments():
    import argparse

    parser = argparse.ArgumentParser(
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)

    parser.add_argument('-i', '--input', default=None, required=True,
        help='A input filename.')

    parser.add_argument('--debug', default=False, action='store_true',
        help='Debug mode. (Show information to debug.)')
    parser.add_argument('--raw-error', default=False, action='store_true',
        help='Debug mode. (Show raw error message.)')

    parser.add_argument('-y', default=None, type=int,
        help='The start line number of a input line. 0-ORIGIN.')
    parser.add_argument('--y2', default=None, type=int,
        help='The end line number of a input line. 0-ORIGIN.')
    parser.add_argument('-d', '--day', default=None, type=int,
        help='The day count to walk.')

    parser.add_argument('--to-today', default=False, action='store_true',
        help='Change the current task day to today. MUST: -y')
    parser.add_argument('--repeat', default=False, action='store_true',
        help='Walk day in the current task with rep:n param. MUST: -y.')
    parser.add_argument('--walk', default=False, action='store_true',
        help='Walk day in the current task. MUST: -y and -d.')
    parser.add_argument('--sort', default=False, action='store_true',
        help='Do sort.')

    args = parser.parse_args()
return args
```

引数は色々ありますが、

- `-i` で .trita ファイルを指定
- `--sort` を指定するとソートを実行する
- あとは各種操作を行うために必要な入力（を受け取る口）をつらつら用意

ざっとこんな感じです。ヘルプの英語はたぶんアレですが見逃してください :sweat:

### タスクの取り扱い
一行で表現されているタスクを、内部的にどう扱うかですが、OOP ……というほどでもないですが、Task クラスにまとめてみました。

`__init__()` 部分を抜粋すると、

```python
class Task:
    def __init__(self, line):
        #      01234567890123456789012345678
        fmt = 'M YYYY/MM/DD DOW HH:MM HH:MM '
        if len(line)<len(fmt):
            abort('Invalid format, length shortage "{0}".'.format(line))

        if line[1]!=' '  or \
           line[12]!=' ' or \
           line[16]!=' ' or \
           line[22]!=' ' or \
           line[28]!=' ':
            abort('Invalid format, wrong space delim pos "{0}".'.format(line))

        self._line = line
        self._sortmark    = line[0:1]
        self._date        = line[2:12]
        self._dow         = line[13:16]
        self._starttime   = line[17:22]
        self._endtime     = line[23:28]
        self._description = line[29:]

        self.complete()

        # extract all options from description
        self._options = {}
        for elm in self._description.split(' '):
            if elm.find(':')==-1:
                continue
            key, value = elm.split(':', 1)
            self._options[key] = value
```

要するに、`M YYYY/MM/DD DOW HH:MM HH:MM ` ← こんな行を泥臭く解釈して、扱いやすいようにもたせてる感じです。

逆に trita ファイルに書き戻す時は、上記の行文字列に戻してます。

```
    def __str__(self):
        return '{0} {1} {2} {3} {4} {5}'.format(
            self._sortmark, self._date, self._dow,
            self._starttime, self._endtime,
            self._description
        )

```

### 日付時刻文字列の取り扱い
Tritask では「日付をN日後に設定する」とか「土日をスキップする」とか、日付時刻に絡む処理が結構ありますが、標準ライブラリの `datetime` のおかげでかなり助かりました。

Python の datetime は素晴らしいと思います。

### ソート処理について
ソートというと `sort` コマンドで一発じゃないか、と思いがちですが、ソート前に色々と処理が必要だったので自製せざるをえませんでした。

コードで示すと以下です。

```python
    if args.sort:
        outfile = infile

        # before sorting
        # --------------
        apply_holding(lines)
        apply_skipping(lines)
        apply_completion(lines)
        apply_timebind(lines)

        # sorting
        # -------
        lines.sort()
```

ソート自体は破壊的メソッドである `list.sort()` を使ってますが、その前に属性（定期タスクやら曜日スキップやら）を解釈させてます。

Tritask において **最もよく行うのがソート操作** なので、特別な解釈が必要な属性系の処理はこのタイミングで行わせています。利用時も「ソートしたら解釈される」と単純明快な挙動になるので、この仕様は正解だったかなと思ってます。

### エラー情報の出力
Windows で Python スクリプトを実行すると黒い画面が出て煩わしいので、Tritask では `pythonw` ランチャーを使っています。が、そうすると今度は何も出ないのでエラー発生時に困ります。

あまり上手い方法が思い浮かばなかったのですが、 **Exception をくるんで、その内容をファイルに吐く** ことにしました。

```python
logfile = os.path.join(MYDIR, 'tritask.log')
if not(os.path.exists(logfile)):
    # new file if does not exists.
    list2file(logfile, [])
loglines = file2list(logfile)

try:
    if args.debug and args.y!=None:
        task = Task(lines[args.y])
    ...
    (中略)
    ...
except Exception as e:
    if args.raw_error:
        raise
    errmsg = 'Type:{0} Detail:{1}'.format(str(type(e)), str(e))
    creationdate = datetime.datetime.today().strftime('%Y/%m/%d %H:%M:%S')
    logmsg = '{0} {1}'.format(creationdate, errmsg)
    loglines.insert(0, logmsg)
    list2file(logfile, loglines)

    # open logfile with system association.
    os.system('start "" "{0}"'.format(logfile))
exit(1)
```

Exception をキャッチした後、出力用の文字列を整えてからファイルに保存し、最後に `os.system('start "" "{0}"'.format(logfile))` でそのファイルを開くところまでやっています。

# おわりに
「エンジニア向けのタスク管理ツール」というテーマを、ポエムも交えてお届けしました。

「タスク管理」は個人的に好きですし、エンジニア界隈ではあまり盛り上がっていない気もするので、もっと盛り上がればいいなぁと思います。