LoginSignup
2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-27

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

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

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

よりよい方法が見つかったのであちらに別エントリをご用意しました。

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/と読んでくれるといいと思います。

コマンドの登録

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

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. 具体的に「そんな長いタイマー使わせるか」と、残り時間がめちゃめちゃ短いものに差し替えられました。 

2
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1