先日、どうしてもdroidVimのためにvimscriptでcronを実装したいのでアラームスクリプト、"vialarm"を作った話をしたのですが、スクリプトの出来は全く満足できるものではありませんでした。問題のすべてはVim自身が持っているautocmdの情報と、vialarmが持っているautocmdの情報を同期させる必要があるためでした。前エントリを書き、本業の金物屋の破綻したスケジュールを案じながら床についてから気がついてしまったんです。
「…どうしてvialarmがタイマー情報を持ってないとだめなんですか?」
ヨシ!
scriptencoding utf-8function!s:timerStart() abort
lets:vialarmTimer= timer_start((60-(localtime()%60))*1000,function('s:getAlarm'))ifv:vim_did_enter
echo '[vialarm]: Start vialarm.'endifendfunctionfunction!s:timerStop() abort
call timer_stop(s:vialarmTimer)
unlet s:vialarmTimer
echo '[vialarm]: Stop vialarm.'endfunctionfunction!s:getAlarm(timer) abort
let now = strftime('%H:%M', localtime())let autocmdText = execute('autocmd User')ifmatch(autocmdText,'^\s\+vialarm_'.now)>=0
execute 'doautocmd User''vialarm_'.now
endifif timer_info(s:vialarmTimer)[0].repeat >0lets:vialarmTimer= timer_start(60000,function('s:getAlarm'),{'repeat':-1})endifendfunctionfunction!s:addOneshot(time, command) abort
tryifmatch(a:time,'^\d\d:\d\d$')<0throw'vialarm_E01'elseif matchstr(a:time,'^\d\d')>23|| matchstr(a:time,'\d\d$')>59throw'vialarm_E02'elseifa: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.'endtryendfunctionfunction!s:showAlarms() abort
let autocmdText = split(execute('1verbose autocmd User'),'\n')[1:]let currentGroup =''let isHit =0letverbose=0
echohl Title
echo '[vialarm]: Current alarm is...'
echohl None
for txt in autocmdText
ifmatch(txt,'^\S')>=0let currentGroup = txt
elseifmatch(txt,'^\s\+vialarm')>=0if currentGroup !=# ''
echohl Title
echo currentGroup
echohl None
let currentGroup =''endif
echo txt
let isHit =match(txt,'\d\d:\d\d$')>=0letverbose=1elseif isHit
echo txt
let isHit =0elseifverbose
echo txt
letverbose=0endifendforendfunctionfunction!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])forkin keys(timerState)->filter('v:val !=? "callback"')
echo printf(' %-10S: %d',k, timerState[k])endforelse
echo '[vialarm]: Vialarm was stopped.'
echohl None
endifendfunctionfunction! vialarm#main(args, isBang) abort
ifa:isBang==# ''ifa:args!=# ''let time = matchstr(a:args,'^\S*')let command = matchstr(a:args,'\s\zs.*$')calls:addOneshot(time, command)elsecalls:showAlarms()endifelseifa:args==? 'start'calls:timerStart()elseifa:args==? 'stop'calls:timerStop()elseifa:args==# ''calls:showTimerInfo()else
echo '[vialarm]: Usage `:Vialarm! [(start|stop)]`'endifendifendfunction
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に関しては公開するプラグインには何が必要かよくわかっていないのでまずそれを調べてから…