Vimが作業を中断させてくれるコマンドをつくってみた

# 背景
人間が集中力を維持できる時間には限界があり、高い集中力を維持できる限界が15分、普通の集中力を維持できる限界が45分、そして集中力を維持できる限界が90分だといいます（参考文献：[読んだら忘れない読書術](https://www.amazon.co.jp/dp/B00W723I3U/)）。

であれば、集中力が切れてぼーっとし始める前に、設定した時間になったら作業を中断させてくれると便利だなと考えました。

付け加えると、作業中の画面が映っていると休憩中も気になってしまうので、画面を隠してくれると嬉しいです。

ここから私が考えた解決策は、「**Vimのタイマー機能でカウントダウンし、終了後にスクリーンセーバーを実行する**」というものです。

# コード

以下が実際のコードです。

```VimScript
"コマンドラインタイマー（Commandline Timer; cmdtimer）
"@param 測る時間（0秒から99分59秒まで）
"実行方法 :Ct {minute} [{second}]
"概要：
"秒単位で指定する時は、{minute}は0にする。
"分単位で指定する時は、[{second}]は指定しなくても良い（0が設定される）。
"0分0秒を指定することで、現在動いているタイマーを破棄出来る。



"ユーザ設定が存在すればデフォルト値で上書きする
let g:cmdtimer_show_screensaver = get(g:,'cmdtimer_show_screensaver',v:false)
let g:cmdtimer_scrfile_path = get(g:,'cmdtimer_scrfile_path','')

let s:running_timer_id = v:null
let s:running_timer_m = v:null
let s:running_timer_s = v:null
com! -nargs=+ Cmdtimer call <SID>CmdtimerMain(<f-args>)
function! s:CmdtimerMain(...)
  let l:min_str = a:1
  "3つ目以降の引数は無視する
  let l:sec_str = (a:0>=2) ? a:2 : "0"

  "### 引数のバリデーションチェック
  if match(l:min_str,"[^0-9]")!=-1 || match(l:sec_str,"[^0-9]")!=-1
    call s:ErrorMsg("ERROR: Either minute or second is not number format.")
    return
  endif
  "引数を数値型に変換する
  let l:min = str2nr(l:min_str,10)
  let l:sec = str2nr(l:sec_str,10)
  "値の範囲をチェックする
  if l:min<0 || l:min>99 || l:sec<0 || l:sec>59
    call s:ErrorMsg("ERROR: Either minute or second is out of range of [0m0s,99m59s].")
    return
  endif

  "0分0秒が指定された場合、現在動いているタイマーを破棄する
  if l:min==0 && l:sec==0
    call s:CmdtimerStop()
    return
  endif

  "タイマーの初期値を表示する処理は、
  "この関数内で行っておかなければならない
  echo "[Timer | ".l:min."min ".l:sec."sec]"
  "他に動いているコマンドラインタイマーがあれば、破棄する
  if s:running_timer_id!=v:null
    call timer_stop(s:running_timer_id)
  endif
  "1秒毎に更新するタイマーを起動する
  let l:num_of_rep = l:min*60 + l:sec
  let l:timer_id = timer_start(1000,function("s:Notifyer"),{"repeat": l:num_of_rep})
  "タイマーの情報を保持しておく
  let s:running_timer_id = l:timer_id
  let s:running_timer_m = l:min
  let s:running_timer_s = l:sec
endfunction

"コマンドラインタイマーを破棄する
"@param 無し
function! s:CmdtimerStop()
  if s:running_timer_id!=v:null
    call timer_stop(s:running_timer_id)
    let s:running_timer_id = v:null
    let s:running_timer_m = v:null
    let s:running_timer_s = v:null
    echo "Timer was aborted."
  else
    call s:ErrorMsg("ERROR: No running timer exists.")
  endif
endfunction

"現在のタイマーを表示する
"@param タイマーID
function! s:Notifyer(timer_id)
  let l:info = timer_info(a:timer_id)
  let l:rep = l:info[0].repeat
  "残りのリピート回数×1秒が残り時間
  "※初回は1秒後に呼び出されることに注意
  let l:min = (l:rep-1)/60
  let l:sec = (l:rep-1) - l:min*60
  "タイマーが止まったら、メッセージの色を変え、
  "ダイアログを出して知らせる
  if (l:rep-1)==0
    call s:WarningMsg("[Timer | ".l:min."min ".l:sec."sec]")
    call confirm(s:running_timer_m."m ".s:running_timer_s."s have passed.","&OK",1,"Info")
    "タイマー情報を初期化する
    let s:running_timer_id = v:null
    let s:running_timer_m = v:null
    let s:running_timer_s = v:null
    "強制的に休憩を挟ませるために、スクリーンセーバーを表示する
    if g:cmdtimer_show_screensaver && g:cmdtimer_scrfile_path!=''
      call system(g:cmdtimer_scrfile_path.' /s')
    endif
  else
    echo "[Timer | ". l:min."min ".l:sec."sec]"
  endif
endfunction

"受け取った文字列をエラーメッセージとして出力する
"@param エラーメッセージ
function! s:ErrorMsg(msg)
  echohl ErrorMsg
  echo a:msg
  echohl None
endfunction

"受け取った文字列を警告メッセージとして出力する
"@param 警告メッセージ
function! s:WarningMsg(msg)
  echohl WarningMsg
  echo a:msg
  echohl None
endfunction
```

例えば、vimrcに

```VimScript
"カウント終了後にスクリーンセーバーを表示する
let g:cmdtimer_show_screensaver = v:true
"表示するスクリーンセーバーのパス
let g:cmdtimer_scrfile_path = 'C:\WINDOWS\System32\Ribbons.scr'
```

などと書くことで、任意のスクリーンセーバーを起動させることができます。

# 蛇足

実を言うと、昔つくったコマンドラインタイマーに、スクリーンセーバーを表示させる処理を追加しただけです。

ダイアログ表示処理をそのまま残したのは、時間経過に気づかずに、意図しないキー入力でスクリーンセーバーを消してしまう恐れがあるからです。

タイマーIDをわざわざ保持しているのは、IDを指定しない`timer_stopall()`を使うと、他のプラグインが起動したタイマーを誤って破棄してしまう恐れがあるからです。