指定時刻にコマンドを実行したい

出先でのメモ、どうしてますか？
せっかくなのでスマホで済ませませんか？
せっかくなのでdroidVim使いませんか？

というわけで、出先のメモをdroidVimで取る事が増えてきたんですが、新規バッファを勝手に保存するにはバッファ名を何とかするためにちょっと手がかかるじゃないですか？というわけでなんとかしたいんです。出来れば17時になんとかしてくれると嬉しいんです。が、android端末だとvimの`+clientserver`どころかcronもない。あるのは`+timers`だけ。いつ起動したのか不明なままタイマーの終了時刻を指定なんて出来ない…ので、さて書きましょう。

## で、出来たものがこちらになる…と思うんですけど。

よりよい方法が見つかったので[あちらに別エントリをご用意しました。](https://qiita.com/astrorobot110/items/c47d4dd53b97be6e9eef)

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

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

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

let s:oneshots = []

function! s:init() abort
	try
		let s:alarms = split(execute('autocmd vialarm'), '[\n]')[2:]
		call map(s:alarms, { _, val -> matchstr(val,'\d\d:\d\d') })
	catch /E216/
		let s:alarms = []
	endtry

	if exists('s:vialarmTimer')
		call timer_stop(s:vialarmTimer)
	endif
	let s:vialarmTimer = timer_start((60-(localtime()%60))*1000, function('s:getAlarm'))

	if v:vim_did_enter
		echo 'Vialarm: initialized.'
	endif
endfunction

function! s:getAlarm(timer) abort
	let now = strftime('%H:%M', localtime())
	if count(s:alarms + s:oneshots, now) > 0
		execute 'doautocmd User' now

		if match(s:oneshots, now) > -1
			call filter(s:oneshots, { key, val -> val !=# now })
		endif
	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'
		endif

		if matchstr(a:time, '^\d\d') > 23 || matchstr(a:time, '\d\d$') > 59
			throw 'vialarm_E02'
		endif

		if a:command ==# ''
			throw 'vialarm_E03'
		endif

		execute 'autocmd vialarm User' a:time '++once' a:command
		call add(s:oneshots, a:time)

		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! 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
			echo 'You can also get vialarm info from ":autocmd vialarm".'
			autocmd vialarm
		endif
	else
		call s:init()
	endif
endfunction

function! vialarm#stop() abort
	call timer_stop(s:vialarmTimer)
	echo 'Vialarm was stopped. execute '':Vialarm'' if you want start vialarm again.'
endfunction

function! vialarm#getTimerInfo() abort
	return timer_info(s:vialarmTimer)[0]
endfunction

function! vialarm#peek() abort
	return s:alarms + s:oneshots
endfunction
```

## トリセツ

出来上がったばかりで「win機で動いたし問題なく動くやろﾎﾟﾁｰ」したものがこちらです。当初、クラスを作ってめちゃめちゃ頑張っていたのですが、その結果86400000msecのタイマーを回そうとしてwin以外の端末にめちゃめちゃ嫌な顔[^1]をされました。何が起こっているのかわからなかったのでvim-jpのslackで質問したところ、良いアドバイスを頂きまして、結果シンプルな方法で実装できました。人柱希望も兼ねて取り急ぎの使い方を…

### …なんて読むん？

ｳﾞｨｱﾗｰﾑでえーんじゃないですか？そう読む根性があれば/(vi|alar)m/と読んでくれるといいと思います。

### コマンドの登録

実行したいコマンドは、`autocmd`の`vialarm`グループに登録する形になります。こんな感じです…

```alarms.vim
augroup vialarm
    autocmd!
    autocmd User 23:00 echo 'テレホ開始'
    autocmd User 08:00 echo 'テレホ終了'
augroup END

Vialarm!
```

autocmdの設定後にスクリプト側で`:Vialarm!`をしていますが、これによってvialarmが初期化され、タイマーが起動します。

### `:Vialarm`コマンド

コマンドの引数は`:Vialarm[!] [{time} {command}]`となっています。

引数付きの起動の場合、内部では{time}引数のチェックの後に`:autocmd vialarm User {time} ++once {command}`が実行されます。その後、vialarmのautocmd発火管理のためにスクリプト内部の変数に発火時間を登録するので、スクリプト外部からタイマーを登録しても発火しないどころか、この状態から`:Vialarm`すると二回目以降の発火でエラーが出ます。

引数なしの起動の場合、単純に`:autocmd vialarm`の結果を出力します。そもそもこのスクリプト自体が何かを管理してこねくり回したるというより、Vim自体が管理出来るものはそちらに任せたい、という発想なのでどちらでもいいかなと思います。

bang付で起動した場合、前述の通りautocmdの登録を再チェックします。引数は無視されます。

## もうちょっと解説

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

`:autocmd vialarm`の結果を切り出して内部リスト`s:alarms`に起動時間を文字列として保管しています。
このテキストがそのままautocmdの`vialarm`グループのパターンになっているのでVimの機能とうまく連動する反面、この手順のせいで`vialarm`グループ以外のグループが使用できなかったり、アラームの追加の度に初期化手順としてこの関数を呼び出さなければならない、等のデメリットも抱えています。前述の通り「Vimが管理出来るものは管理してもらう」ようにした結果ですが、不細工ではあるのでスマートにautocmdと連動しながらやれるいい方法があればいいのですが…

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

`localtime()`を`vialarm`グループのパターン文字列にして、内部変数`s:alarms`と後述の`s:oneshots`リストに含まれていれば`:doautocmd`で発火という手順を取っています。`:vialarm`外からonceなコマンドを登録するとおかしなことになるのは`s:oneshots`に発火時間が登録されていないのが原因です。また、`:autocmd vialarm`をしても出力からonce付きかどうかが判断できないので、この状態から初期化すると`s:alarms`に保管した時刻のautocmdが消えている、というケースが起こります。うーん…

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

前述の通り引数のチェックをしてからautocmdの登録と、onceなiautocmdだけを管理する内部リスト`s:oneshots`に時刻を保管しています。前述の通りトラブルの素です。

### `vialarm#main()`関数

コマンド`:Vialarm`のための関数です。
ワガママですが機能の少ないスクリプトにいちいち`:VialarmInit`、`:VialarmOneshot`みたいな長いコマンドを用意する気になれなかったので機能を詰めてしまいました。良くない方法なのですが、実際に機能が増えてなんとかする必要があるかどうか…

### その他の関数

とりあえず止めときたい時の`vialarm#stop()`関数、ちょっとタイマー確認したい時用の`vialarm#getTimerInfo()`関数、vialarmが発火時間を把握しているか確認したい時用の`vialarm#peek()`関数です。
ぶっちゃけエラー確認の名残でまぁ、使う人が困ったらこの辺あるほうが助かるでしょ？的に残しています。

## 余談

vim-jpの皆様から「droidVimのためにvim内部でcronを実装する」という発想に、半笑いで「頑張れ」と励ましを受けていい気になってるので、細かいところを精査してgitに登録しておこうかなと思っています。

[^1]: 具体的に「そんな長いタイマー使わせるか」と、残り時間がめちゃめちゃ短いものに差し替えられました。