Vimで作業時間を計測→モチベ維持作戦

# About
この記事は[K3 Advent Calender](https://qiita.com/advent-calendar/2019/k3-calendar)20日目の記事です。区切りだよ(o・∇・o)

今回はVimにはまったり、VimScriptいじったりしてみていたので、それを題材にしてみました。



# 今回行ったこと
* 時報機能を追加
* 作業開始時間を記録＆時報を呼ぶと何分やってるかを教えてくれる
* なんとなく労ってくれる

# 経緯
1. HaskellでghcもghciもVimから簡単に呼べるようにしたい→自作コマンドを作ろう
2. あれ、Vim scriptさん凄く色々できそうじゃない？
3. なんかやろう

こんな経緯からお察しの通り、私自身はここ1ヶ月くらいでVim熱が一気に高まった弱々Vimmerです。何かアドバイス有りましたら頂ければ嬉しいです。

# 環境
* Windows 10 Education
* Vim 8.0.596


# 前準備

実際にコーディングに入る前の段階です。

## Scriptファイルの作成
今回はUserディレクトリの下に各種設定用のフォルダを作り、その下に`mycmd.vim`を作成しました。

この中身をいじっていくのが中心となります。

## .vimrcの設定
上記のことを.vimrcに教えてやり、vim起動時に有効になるようにします。

常時起動にしない場合は普通に各コマンドと同じようにコマンド打てばOKですが、私は面倒でした。

```
source C:\Users\XXX\setting\mycmd.vim
```

## 基礎知識
とりあえず、必要だった知識を先にまとめます。スルーしていることがあるかもしれないです…そしたらすみません………
ここから先のことは試すときは`mycmd.vim`に書くのをおすすめします。

[Vimスクリプト基礎文法最速マスター](https://thinca.hatenablog.com/entry/20100201/1265009821)←この辺を参考にさせていただきました。

### 独自コマンドの作成

とりあえず、`Hello, (o・∇・o)`と出力するだけのコマンド`Mco`を作ってみましょう。独自コマンドは大文字から始めなければならない規則があるようです。

```vimscript
command Mco echo "Hello, (o・∇・o)"
```

簡単ですね。`command [コマンド名] [実行する内容]`です。

echoは単純に出力するやつなので、Cの`printf`とか、Javaの`System.out.println`とか、pythonの`print`とか、JSの`console.log`とか、そのイメージですね。

### 関数を作ろう＆ついでに変数

Vim scriptさん、関数を定義できます。正直この辺りナメてました。すみません。

上のやつを`mcoecho`とか関数にしてみます。

```vimscript
function s:mcoecho()
    echo "Hello, (o・∇・o)"
endfunction
```

あまり難しいことはないですね。よくある関数定義と一緒です。`function`を`function!`と末尾にエクスクラメーションマークを付けると既存の関数を上書きしてくれるようですが、あまり既存のものを壊すのが好きではないので、なんとなく付けていません。かぶらないような名前をつけていきたい(願望)。

関数名の頭の`s:`についてはこの辺りを参考にしました。要するに`private`というか、そんなもんです。変数についての話ですが関数も大方同じみたい？
[vimスクリプトの変数](https://kaworu.jpn.org/vim/vim%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%83%88%E3%81%AE%E5%A4%89%E6%95%B0)

変数の代入時は頭に毎度letが必要です。この辺りも注意。宣言ではなく、代入の頭に必要です。むしろ宣言は不要。一度上のリンクで確認頂くのが良いと思います。

### 引数を取る関数＆while/for/if

引数も勿論取れます。

```vimscript
function s:mcoechorepeat(n)
    let i = 0
    while i < a:n
        echo "Hello, (o・∇・o)"
        let i += 1
    endwhile
endfunction
```

引数を取るときは`n`に対して`a:n`のように頭に接頭辞がいります。ここだけご注意。
ぬるっと使いましたが、`while`は大方の言語と似ています。括弧がいらない。
ところで閉じるときは大体`endXXXX`です。BASICを思い出す。

この引数、実は可変長を取れます。`...`で示します。

```vimscript
function s:multifunc(...)
    [やること]
endfunction
```
こいつ、`a:0`で引数の個数を、以降は`a:1`, `a:2`と順番に引数が格納されていきます。便利です。詳細は最初に提示したサイトさんに掲載されていました。

`for`はリストの中身を回します。pythonの`for`とか各種言語の`foreach`に近いですね。

```
for i in [1,2,3,4,5]
    echo "Hello, (o・∇・o)" . i
endfor
```

この出力はこんな感じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191019/9988ccba-6690-320e-b26a-e9e14f319c4b.png)

最後にifはこんな感じです。

```vimscript
for i in [1,2,3,4,5]
    if i % 2 == 0
        echo "Even"
    elseif i == 1
        echo "One"
    else
        echo "Odd"
    endif
endfor
```


### 関数を独自コマンドとして適用

超大事。これできないとあんまり意味なくなっちゃう。

```vimscript
command Mco call s:mcoecho()

function s:mcoecho()
    echo "Hello, (o・∇・o)"
endfunction
```

commandで使うときは`call`が必要です。関数内で関数を呼び出す等のときは`call`なしで普段の手続き型言語のように書いてOKです。


### 文字の結合
文字の結合は`.`です。

```vimscript
let strQiita = "Qii" . "ta"
```

って感じですね。数値とか大丈夫かな～って思ってたんですけど結構柔軟に扱ってくれます。良い子です。

# モチベ維持作戦スタート

ざっくり説明したんだか他のサイトさんに丸投げしたんだかわかりませんが、とりあえず本題に入ります。以降メインで実行させる関数は`s:nowtime`です。

## 時間を言ってもらおう

まずは時間を取得しなければなりません。`strftime`という関数を使います。

どこかの言語で時刻を扱ったことのある人なら見たことはあるかもしれない形で扱えます。

```vimscript
let nowtime = strftime("%Y-%m-%d %H:%M:%S")
```

とりあえずこれで現在時間が`2019-11-25 11:00:00`という感じにnowtime変数に代入されます。第2引数としてUNIX時間をつっこんでやるとその時間出したりもできます。

```vimscript
command Mtime call s:nowtime()

function s:nowtime()
    let nowtime = strftime("%m月%d日%H時%M分")
    echo ("(o・∇・o)<いま、" . nowtime . "だよ～")
endfunction
```
じゃあこれを使って、

これで、`:Mtime`と打ってやればとりあえず時報は言ってくれるようになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191019/589e1a25-f1a6-558e-856b-91147e05d12f.png)

こんな感じ。可愛いですね。

## 作業時間を記録しよう
記録はUNIX時間で行いました。経過時間の計算の際に、現在時刻のUNIXとの差を求めていい感じにやると楽できるかなぁと思ったので。

というところで現在のUNIX時間を取ってきましょう。`localtime()`で持ってきます。

```vimscript
let nowUnix = localtime()
```

これで`nowUnix`には`1574698112`みたいなのが入ってくれます。

とりあえず単純に記録機能を追加させてみます。

```vimscript
command Mtime call s:nowtime()

let s:timerec = -1
function s:nowtime()
    let nowtime = strftime("%m月%d日%H時%M分")
    echo ("(o・∇・o)<いま、" . nowtime . "だよ～")
    let s:timerec = localtime()
endfunction
```

はい、これで記録できますね。ただ無限に記録させ続けてしまう……コマンドでなんとかしたいですね。

引数に`r`を取るときだけ記録させてみます。


```vimscript
command -nargs=? Mtime call s:nowtime(<f-args>)

let s:timerec = -1
function s:nowtime(...)
    let nowtime = strftime("%m月%d日%H時%M分")
    echo ("(o・∇・o)<いま、" . nowtime . "だよ～")
    if a:0 >= 1
        if a:1 == "r"
            let s:timerec = localtime()
        endif
    endif
endfunction
```

ちょっと面倒になりました。まず1行目コマンド定義の行。

コマンドに引数を取るときは`-nargs=1`のように、何個引数を取るか書かねばなりません。引数が可変の場合は`-nargs=?`です。今回は0個or1個なので可変ですね。`nowtime()`は`nowtime(<f-args>)`と変えとく必要がある。

そして、`function`側も引数に可変を表す`...`を入れる必要があります。内部では引数を持つ場合1つ目のif文がTrue,その引数が`"r"`であれば`s:timerec`に`localtime()`を入れています。


## 作業終了させる
同様に、引数に`b`を取るときだけ作業終了の処理をします。

これは`s:timerec`と現在時刻の差を出力、`s:timerec`を初期化する、の２点ができればOKです。

とりあえず`s:timerec`と現在時刻の差を整形してくれる関数を作りましょう、ということで、引数に現在時刻をとって「4時間17分56秒」とか返してくれる`s:getElapsedTime`を作りました。

```vimscript
function s:getElapsedTime(loctime)
    let worktime = a:loctime - s:timerec
    let wsec = worktime % 60
    let worktime = worktime / 60
    let wmin = worktime % 60
    let whour = worktime / 60
    let strtime = wsec . "秒"
    if whour > 0
        let strtime = whour . "時間" . wmin . "分" . strtime
    elseif wmin > 0
        let strtime = wmin . "分" . strtime
    endif
    return strtime
endfunction
```

特に難しいことはなく。`let`と`if`文べたーっと書いて`return`しているくらいです。`return`は関数の戻り値を指定する、多くの言語であるやつです。

これを使って、`(o・∇・o)`に経過時間を喋らせてみましょう。

```vimscript
        elseif a:1 == "b"
            if s:timerec == -1
                echo("(o・∇・o)<時間が記録されてないよ～")
                return
            end
            let strtime = s:getElapsedTime(loctime)
            echo ("(o・∇・o)<いま、" . nowtime . "！ " . strtime . "作業したよ～おつかれさま～")
            let s:timerec = -1
```
`s:timerec`は記録されていないときに`-1`にしています。スタートしてないのに終了しようとしたら`時間が記録されてないよ～`と煽ってあげましょう。

正しく実行されるとこんな感じになります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191019/c81e4400-c7ad-a1d8-f4aa-91ffd50fe27e.png)

可愛いですね。

## 単に時間を出力するコマンドの修正

こうなると、単に時間を出力させるのも味気ないですし、そのときに今何分やったかを知らせてほしいですね。
ついでに、先のコードだと、2回も時間を出力させられてしまうので、そこも上手く修正したいところです。

ざっくり、`s:nowtime()`関数の概形はこんな感じにしましょう。

```vimscript
let s:timerec = -1
funtion s:nowtime(...)
   if [引数が1つ以上]
      if [引数が"r"]
         " 記録スタート
      elseif [引数が"b"]
         " 記録終了
      end
   else 
      if [時間が記録されていない]
         " 単に時間を出力
      else
         " 時間と一緒に経過時間も表示
      end
   end
endfunction
```

## 完成！

これらを踏まえて最終的な`s:nowtime()`はこうなります。

```vimscript
let s:timerec = -1
function s:nowtime(...)
    let nowtime = strftime("%m月%d日%H時%M分")
    let loctime = localtime()
    if a:0 >= 1
        if a:1 == "r"
            echo ("(o・∇・o)<すたーと～ いま" . nowtime . "です～がんばっていこ～")
            let s:timerec = loctime
        elseif a:1 == "b"
            if s:timerec == -1
                echo("(o・∇・o)<時間が記録されてないよ～")
                return
            end
            let strtime = s:getElapsedTime(loctime)
            echo ("(o・∇・o)<いま、" . nowtime . "！ " . strtime . "作業したよ～おつかれさま～")
            let s:timerec = -1
        end
    else
        if s:timerec == -1
            echo ("(o・∇・o)<いま、" . nowtime . "だよ～")
        else
            let strtime = s:getElapsedTime(loctime)
            echo ("(o・∇・o)<いま、" . nowtime . "！ " . strtime . "作業したよ～がんばっていこ～")
        end
    end
endfunction
```

## 結論

* 可愛い
* 進捗は言うても変わんない
* でも可愛い