指定時間にコマンドを実行したいのでもうちょっと頑張った

先日、[どうしてもdroidVimのためにvimscriptでcronを実装したい](https://qiita.com/astrorobot110/items/9bd417ad0012ed5e3472)のでアラームスクリプト、"vialarm"を作った話をしたのですが、スクリプトの出来は全く満足できるものではありませんでした。問題のすべてはVim自身が持っているautocmdの情報と、vialarmが持っているautocmdの情報を同期させる必要があるためでした。前エントリを書き、本業の金物屋の破綻したスケジュールを案じながら床についてから気がついてしまったんです。

「…どうしてvialarmがタイマー情報を持ってないとだめなんですか？」

## ヨシ！

```autocmd/vialarm.vim
scriptencoding utf-8

function! s:timerStart() abort
	let s:vialarmTimer = timer_start((60-(localtime()%60))*1000, function('s:getAlarm'))

	if v:vim_did_enter
		echo '[vialarm]: Start vialarm.'
	endif
endfunction

function! s:timerStop() abort
	call timer_stop(s:vialarmTimer)
	unlet s:vialarmTimer
	echo '[vialarm]: Stop vialarm.'
endfunction

function! s:getAlarm(timer) abort
	let now = strftime('%H:%M', localtime())
	let autocmdText = execute('autocmd User')
	if match(autocmdText, '^\s\+vialarm_'.now) >= 0
		execute 'doautocmd User' 'vialarm_'.now
	endif

	if timer_info(s:vialarmTimer)[0].repeat > 0
		let s:vialarmTimer = timer_start(60000, function('s:getAlarm'), {'repeat':-1})
	endif
endfunction

function! s:addOneshot(time, command) abort
	try
		if match(a:time, '^\d\d:\d\d$') < 0
			throw 'vialarm_E01'
		elseif matchstr(a:time, '^\d\d') > 23 || matchstr(a:time, '\d\d$') > 59
			throw 'vialarm_E02'
		elseif a:command ==# ''
			throw 'vialarm_E03'
		endif

		execute 'augroup vialarm_oneshots'
		execute 'autocmd User' 'vialarm_'.a:time '++once' a:command
		execute 'augroup END'

		echo printf('[vialarm]: Added alarm in %s.', a:time)

	catch /vialarm_E01/
		echoerr '[vialarm]: Error: Time format must be ''HH:MM''.'
	catch /vialarm_E02/
		echoerr '[vialarm]: Error: No such time. :('
	catch /vialarm_E03/
		echoerr '[vialarm]: Error: Command is empty.'
	endtry
endfunction

function! s:showAlarms() abort
	let autocmdText = split(execute('1verbose autocmd User'), '\n')[1:]

	let currentGroup = ''
	let isHit = 0
	let verbose = 0

	echohl Title
	echo '[vialarm]: Current alarm is...'
	echohl None

	for txt in autocmdText
		if match(txt, '^\S') >= 0
			let currentGroup = txt
		elseif match(txt, '^\s\+vialarm') >= 0
			if currentGroup !=# ''
				echohl Title
				echo currentGroup
				echohl None
				let currentGroup = ''
			endif
			echo txt

			let isHit = match(txt, '\d\d:\d\d$') >= 0
			let verbose = 1
		elseif isHit
			echo txt
			let isHit = 0
		elseif verbose
			echo txt
			let verbose = 0
		endif
	endfor
endfunction

function! s:showTimerInfo() abort
	echohl Title
	if exists('s:vialarmTimer')
		echo '[vialarm]: Vialarm is running. Current timer information is...'
		echohl None

		let timerState = deepcopy(timer_info(s:vialarmTimer)[0])

		for k in keys(timerState)->filter('v:val !=? "callback"')
			echo printf('  %-10S: %d', k, timerState[k])
		endfor
	else
		echo '[vialarm]: Vialarm was stopped.'
		echohl None
	endif
endfunction

function! vialarm#main(args, isBang) abort
	if a:isBang ==# ''
		if a:args !=# ''
			let time = matchstr(a:args, '^\S*')
			let command = matchstr(a:args, '\s\zs.*$')
			call s:addOneshot(time, command)
		else
			call s:showAlarms()
		endif
	else
		if a:args ==? 'start'
			call s:timerStart()
		elseif a:args ==? 'stop'
			call s:timerStop()
		elseif a:args ==# ''
			call s:showTimerInfo()
		else
			echo '[vialarm]: Usage `:Vialarm! [(start|stop)]`'
		endif
	endif
endfunction
```

```plugin/vialarm.vim
scriptencoding utf-8

command -bang -nargs=* -complete=command Vialarm call vialarm#main(<q-args>, '<bang>')

Vialarm! start
```

## ご安全に！

というわけでvialarm自身はタイマーを回す機能とautocmdからvialarmに関するものピックアップするだけまで機能を削ぎ落としました。機能が削ぎ落とされたのでコマンドの記述も相変わらずbangで詰め詰めになっています。

### bangなしの`:Vialarm`

bangなしではvialarmが発火させるオートコマンドに関する操作を行います。

引数なしの起動でパターン名に'vialarm-'のprefixがついたユーザーコマンドを列挙します。内部的には`:1verbose autocmd User`を実行して表示をコントロールしているので、登録元も分かるようになっています。

引数として`{time} {command}`を取った場合は{time}のエラーを弾いた後、`:autocmd vialarm-oneshot User vialarm_{time} ++once {command}`を実行します。

### bang付きの`Vialarm!`

bang付きではvialarmが起動したタイマーに関する操作を行います。

引数なしでは現在のタイマーの情報が出力されます。

引数として`(start|stop)`を取った場合、それなりにタイマーをスタート/ストップさせます。

## どうして関数が増えたのですか？

vialarmがアラームの情報を持たないので、その都度autocmdから情報を引き出す手順が必要になりました。とはいえ`s:getAlarm()`関数の仕事はむしろ減って、autocmdがonceかどうかも判断せず、`:autocmd User`の結果から現在時刻のアラームを検索して発火させるだけになりました。vialarmがタイマーを管理する手順を省いたのでアラーム情報を解析しやすい状態にしておく必要もなくなり、vialarmグループからも解放されました。この時点ではスクリプトの行数は減っていたんです。

問題は、逆に`:autocmd vialarm`のような方法でお手軽にアラームの情報を引き出せなくなってしまったんです。結局より密な解析をするための関数、`s:showAlarms()`関数を用意しました。

### `s:showAlarms()`関数

autocmdの列挙モード内で`User`イベントを列挙する場合、パターンにマッチパターンが使えません。つまり、`:autocmd User`による~~結果が何行になるか曖昧なクッソ~~出力をうまく切り出す必要があります。そのための`isHit`？あとついでなのでそのための`verbose`？`if~elseif~elseif~`でぺそぺそと分岐を何とかするなら、いっそここでコントロールする行が増えたって別段かめへんかという感じで`:1verbose`を足しました。

### `s:showTimerInfo()`関数

vialarmが満足する形で動くか否かに関しては、`s:showAlarms()`関数の時点で満足したのですが、`:Vialarm`コマンドに機能を詰め込む過程でbang付き、引数なしの状態が余ったので機能を追加するという本末転倒なことをしてしまっています。まぁ、タイマーの開始/終了に引数を使って`:Vialarm! stop`のような形をとったので、引数なしならタイマー情報が出てくるのが自然でしょ、というノリです。当初引数無しで「タイマーのトグル」をしようかとか考えましたが、冷静になって現在の状態も引き出せずにアラームをトグルさせようとするのは、人間としての資質に関わることに気がついたのでやめました。

## どうして？……どうしてですかね……

久しぶりにガッツリvimscriptが書きたかったので、vim充出来たことに満足しています。gitに関しては公開するプラグインには何が必要かよくわかっていないのでまずそれを調べてから…