どうしてもvimで猫と戯れたい！

# 概要
$$\huge{どうしてもvimで猫と戯れたい！}$$$$そう思ったことはありませんか？$$$$\large{ありますよね？}$$$$\huge{そう、あるんですよ。}$$
ということでそんなプラグインを目指して開発をしていこうと思い立ちました。
本記事はとりあえずのプロトタイプを作成したのでその覚書になります。

リポジトリは[こちら](https://github.com/kuroitu/withcat.vim)
どなたでもお気軽にコメント・プルリクいただけると嬉しいです。

なお、このプラグインはvimのpopup機能を活用したものですので、popup機能が利用できるvimでご利用ください。

![cat_gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/640911/cf942d6c-e442-ce36-12df-1372c5f35107.gif)


# 目次
- [WithCat](#withcat)
    - [`plugin`ディレクトリ](#pluginディレクトリ)
    - [`autoload`ディレクトリ](#autoloadディレクトリ)


# WithCat
今回のプラグイン、[WithCat](https://github.com/kuroitu/withcat.vim)はvimにpopup機能が追加された際に公開された[killersheep](https://github.com/vim/killersheep)をもとに作成しています。
ディレクトリ構成は以下の通りとなっています。

```console
.
├── LICENSE
├── README.md
├── autoload
│   └── withcat.vim
└── plugin
    ├── cat_silhouette.vim
    └── withcat.vim
```

使用方法は至って簡単で、vimを開いてノーマルモードにて

```console
:WithCat
```

と入力すればOKです。

##`plugin`ディレクトリ
まずは`plugin`ディレクトリの中身から解説していきます。

```plugin/withcat.vim
if get(g:, 'loaded_withcat', 0)
    finish
endif
let g:loaded_withcat = 1

let s:dir = expand('<sfile>:h')

command WithCat call s:StartWithCat()

func s:StartWithCat()
    " Check features before loading the autoload file to avoid error messages.
    if !has('patch-8.1.1705')
        call s:Sorry('Sorry, This build of Vim is too old, you need at least 8.1.1705')
        return
    endif
    if !has('textprop')
        call s:Sorry('Sorry, This build of Vim is lacking the +textprop feature')
        return
    endif
"   if &lines < 45
"       call s:Sorry('Need at least a terminal height of 45 lines')
"       return
"   endif

    " The implementation is in an autoload file, so that this plugin doesn't
    " take much time when not being used.
    call withcat#Start(s:dir)
endfunc

func s:Sorry(msg)
    echohl WarningMsg
    echo a:msg
    echohl None
endfunc
```

基本はkillersheepのものを流用しています。ただし、ウィンドウサイズの制限はとりあえず省いてあります。vimのpopupウィンドウはウィンドウサイズに収まらなければトリミングされる形になりますので、サイズがあまりに小さい場合は後にGIFが途切れてしまう可能性があります。

この`plugin/withcat.vim`はコマンド実行時に1度だけ読み込まれるファイル(らしい)で、エラー文だけ表示し、エラーがなければ`withcat#Start`関数を呼び出してメニュー画面を開きます。

<details><summary>`plugin/cat_shilhouette`</summary><div>

```plugin/cat_shilhouette.vim
let g:CATSHILHOUETTE = [
    \[
    \ '                                                            ',
    \ '                               =?7I=~             ~~        ',
    \ '                            =NMMMMMMMMMD+      :+OMO:       ',
    \ '                          ~NMMMMMMMMMMMMMMMNNMMMMMMMM=      ',
    \ '                        :DMMMMMMMMMMMMMMMMMMMMMMMMMMMN=     ',
    \ '                      IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM8:     ',
    \ '                  :$MMMMMMMMMMMMMMMMMMMMMMMMMMMM?           ',
    \ '             :~ZMMMMMNI :DMMMMMMMMMMMMMMMMMNN$:             ',
    \ '       :+8NMMMMMMMO~     =NMMMMMMM?  +MMD~                  ',
    \ '     8MMMMMMM8+:           :?OMMMM+ =NMM~                   ',
    \ '                             :DMMZ  7MM+                    ',
    \ '                              ?NMMMMMMD:                    ',
    \ '                                 :?777~                     ',
    \],
    \[
    \ '                                                            ',
    \ '                                                     :O~    ',
    \ '                                          +I777ZDNMMMMMI    ',
    \ '                        :+Z8DDDNNNNDD88NMMMMMMMMMMMMMMMM$   ',
    \ '                     +DMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM=  ',
    \ '                  :$MMMMMMMMMMMMMMMMMMMMMMMMMMMMMN888DMM8:  ',
    \ '                :OMMMMMMMMMMMMMMMMMMMMMMMMMMMMMD            ',
    \ '              ?NMMMMMMMMMMMMMMMMMMMMMMMMD?~=DMMMI           ',
    \ '    7MMMMMMMMMMMMZ+NMMMMMMMMMNNNNN87?~: +NDDMMN~            ',
    \ '     ~?$OZZI=:   7MMMMMMMMMN=            =DM8:              ',
    \ '                  8MMN888$:                                 ',
    \ '                  :DMMD$                                    ',
    \ '                    =7$=                                    ',
    \],
    \[
    \ '                                                            ',
    \ '                                                            ',
    \ '                         ~~++?????IIIIIIII7$77I?+I$$77OD:   ',
    \ '              ~7ODNNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM7   ',
    \ '         ~ZNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM?  ',
    \ '      :$MMMMZ=?MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM$  ',
    \ '    =NMM8~  :OMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM8: :~77   ',
    \ '  =NMM+  :DMMMMMMMMMMMMMN+     :=OMMMMMMMMMMD$8MMMMMD?      ',
    \ ' OMN=  ?NMMMMMMMMMMNZ+::             ::~::      IMMMMMMM$   ',
    \ ' ::  ZMMMMMMN?:                                =MMO:  =$~   ',
    \ '    ~8Z~DMN=                                                ',
    \ '                                                            ',
    \ '                                                            ',
    \],
    \[
    \ '                                                            ',
    \ '                      :IZOZI:                               ',
    \ '                   INMMMMMMMMMD+                     ?:     ',
    \ '                =DMMMMMMMMMMMMMMMM8$II7$OO87:  :=ODDMM+     ',
    \ '             :OMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM8    ',
    \ '           =NMMD+ 8MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMI   ',
    \ '        :8MMMZ7DDDMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM7   ',
    \ '      +NMMM7 ~MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM7           ',
    \ '   :7MMMM7   +MMNMMMMM$: :?8NNMMMMMMMMMMNNMMMMMN:           ',
    \ '  =DMMN?    :8MM~  :                      :8MMMMMMMMN8?:    ',
    \ '   =~      8MMM7                             ?8MMMD+?ZNM?   ',
    \ '            +~                                  :$NMMN7     ',
    \ '                                                   :IDD=    ',
    \],
    \[
    \ '                                                            ',
    \ '                            ~~:                             ',
    \ '                        =ONMMMMMMD$~                        ',
    \ '                     +8MMMMMMMMMMMMMNZ:           :         ',
    \ '                  ~8MMMMMMMMMMMMMMMMMMMMDD8DDDNDDM8         ',
    \ '               =OMMMM$DMMMMMMMMMMMMMMMMMMMMMMMMMMMM+        ',
    \ '           :OMMMMNI:  NMMMMMMM$ZNMMMMMMMMMMMMMMMMMM8:       ',
    \ '        ~DMMMNI:    :NMMMMMMMZ   =NMMMMMMMMMMMMMMMMM=       ',
    \ '      =NMMN+         OMMMMMD~      :ZNMMMMMMMMO  :+:        ',
    \ '      :?=:            +MM?            7MM8 +NMMMI:          ',
    \ '                       :NM8=           ZM$    :ZNMMMD       ',
    \ '                         ?$=           :DMO:       II       ',
    \ '                                        :I+                 ',
    \]
\]
```
</div></details>

`cat_shilhouette.vim`には猫のシルエットをアスキーアートにしたものを用意しています。`g:`変数はグローバル変数で、プラグイン内のどこでも参照することができる変数です。ちなみによく使う`s:`変数はスクリプト変数で、そのスクリプトファイル内であればどこからでも参照できる変数です。
注意点としては、現状自動でサイズを合わせたりするような機能はありませんので、同じ変数内のそれぞれのアスキーアートの行・列数は同じにしておかなければなりません。空行やスペースで行・列数を揃えておきましょう。

## `autoload`ディレクトリ
`autoload`ディレクトリにはメインとなるスクリプトが入っています。

```autoload/withcat.vim
let s:did_init = 0

"Main
function! withcat#Start(aadir)
    let s:dir = a:aadir

    if !s:did_init
        let s:did_init = 1
        call s:Init()
    endif

    call s:Clear()
    call s:Intro()
endfunction


func s:Init()
    hi def catSun ctermbg=red guibg=red
    hi def catSky ctermbg=blue guibg=blue
    hi def catGrass ctermfg=green guibg=green
endfunc


func s:Clear()
  if s:intro_timer
    call timer_stop(s:intro_timer)
    let s:intro_timer = 0
  endif
  call popup_clear()
endfunc


func s:NoProp(text)
  return #{text: a:text, props: []}
endfunc


let s:start_title = 10
func s:Intro()
    hi CatTitle cterm=bold gui=bold
    hi introHL ctermbg=cyan guibg=cyan
    call prop_type_delete('catTitle')
    call prop_type_add('catTitle', #{highlight: 'CatTitle'})
    call prop_type_delete('introHL')
    call prop_type_add('introHL', #{highlight: 'introHL'})
    let s:intro = popup_create([
            \   #{text: '         I want to play with a cat!',
            \     props: [#{col: s:start_title, length: 26, type: 'catTitle'}]},
	        \   s:NoProp(''),
	        \   s:NoProp('options:'),
            \   #{text: '     1       cat shilhouette',
            \     props: [#{col: 6, length: 1, type: 'catTitle'}]},
	        \   #{text: '   <Esc>     quit (colon also works)',
            \     props: [#{col: 4, length: 5, type: 'catTitle'}]},
	        \   s:NoProp(''),
	        \   s:NoProp('during doing:'),
            \   #{text: '     q       quit',
            \     props: [#{col: 6, length: 1, type: 'catTitle'}]},
            \   #{text: '   ELSE      stop 5 sec and can edit file',
            \     props: [#{col: 4, length: 4, type: 'catTitle'}]},
	        \   s:NoProp(''),
	        \   #{text: 'Now press  optionsNo  to start or  x  to exit',
            \     props: [#{col: 12, length: 8, type: 'catTitle'},
            \             #{col: 36, length: 1, type: 'catTitle'}]},
	        \ ], #{
	        \   filter: function('s:IntroFilter'),
	        \   callback: function('s:IntroClose'),
	        \   border: [],
	        \   padding: [],
	        \   mapping: 0,
	        \   drag: 1,
	        \   close: 'button',
	        \ })
    call s:IntroHighlight(0)
endfunc


func s:IntroFilter(id, key)
    if a:key == '1'
        call s:Clear()
        call s:CatWalk(g:CATSHILHOUETTE)
    elseif a:key == 'x' || a:key == 'X' || a:key == "\<Esc>"
        call s:Clear()
    endif
    return 1
endfunc


func s:IntroClose(id, res)
    call s:Clear()
endfunc


const s:introHL = [[s:start_title, 1],
                \  [s:start_title+2, 4],
                \  [s:start_title+7, 2],
                \  [s:start_title+10, 4],
                \  [s:start_title+15, 4],
                \  [s:start_title+20, 1],
                \  [s:start_title+22, 4]]
let s:intro_timer = 0
func s:IntroHighlight(idx)
    let idx = a:idx
    if idx >= len(s:introHL)
        let idx = 0
    endif
    let buf = winbufnr(s:intro)
    call prop_remove(#{type: 'introHL', bufnr: buf}, 1)
    call prop_add(1, s:introHL[idx][0],
            \     #{length: s:introHL[idx][1], bufnr: buf, type: 'introHL'})
    let s:intro_timer = timer_start(300, { -> s:IntroHighlight(idx + 1)})
endfunc


func s:CatWalk(walker)
    call s:Clear()
    " Init
    let s:popUpWindow = popup_create('', #{
            \ line: 1,
            \ col: 1,
            \ })
    let s:status = 1
    "call win_execute(s:popUpWindow ,'setlocal filetype=withcatHL')

    while s:status
        for i in range(len(a:walker))
            call popup_settext(s:popUpWindow, a:walker[i])
            redraw
            if getchar(0)
                let s:status = 0
                break
            endif
            sleep 100m
        endfor
    endwhile

    "call popup_close(s:popUpWindow)
    "call s:CatWalk()
    if getchar(0) == "q"
        call s:Clear()
        let s:popUpWindow = popup_create([
                \   #{text: 'Good Vim!',
                \     props: [#{col: 1, length: 9, type: 'catTitle'}]},
                \ ], #{
	            \   border: [],
	            \   padding: [],
	            \   mapping: 0,
	            \   drag: 1,
	            \   close: 'button',
                \ })
        call timer_start(2000, { -> s:Clear() })
    else
        call timer_start(5000, { -> s:CatWalk(a:walker) })
    endif
endfunc
```

こちらはkillersheepのコードのうち必要な部分だけをコピペしてきました。

<details><summary>メイン部分</summary><div>

```autoload/withcat.vim
let s:did_init = 0

"Main
function! withcat#Start(aadir)
    let s:dir = a:aadir

    if !s:did_init
        let s:did_init = 1
        call s:Init()
    endif

    call s:Clear()
    call s:Intro()
endfunction


func s:Init()
    hi def catSun ctermbg=red guibg=red
    hi def catSky ctermbg=blue guibg=blue
    hi def catGrass ctermfg=green guibg=green
endfunc


func s:Clear()
  if s:intro_timer
    call timer_stop(s:intro_timer)
    let s:intro_timer = 0
  endif
  call popup_clear()
endfunc
```
</div></details>

`s:Init`関数ではハイライトを設定していますが、現状は使用していません。
`s:Clear`関数でpopupウィンドウを削除し、`s:Intro`関数へと処理が移ります。

<details><summary>メニュー</summary><div>

```autoload/withcat.vim
func s:NoProp(text)
  return #{text: a:text, props: []}
endfunc


let s:start_title = 10
func s:Intro()
    hi CatTitle cterm=bold gui=bold
    hi introHL ctermbg=cyan guibg=cyan
    call prop_type_delete('catTitle')
    call prop_type_add('catTitle', #{highlight: 'CatTitle'})
    call prop_type_delete('introHL')
    call prop_type_add('introHL', #{highlight: 'introHL'})
    let s:intro = popup_create([
            \   #{text: '         I want to play with a cat!',
            \     props: [#{col: s:start_title, length: 26, type: 'catTitle'}]},
	        \   s:NoProp(''),
	        \   s:NoProp('options:'),
            \   #{text: '     1       cat shilhouette',
            \     props: [#{col: 6, length: 1, type: 'catTitle'}]},
	        \   #{text: '   <Esc>     quit (colon also works)',
            \     props: [#{col: 4, length: 5, type: 'catTitle'}]},
	        \   s:NoProp(''),
	        \   s:NoProp('during doing:'),
            \   #{text: '     q       quit',
            \     props: [#{col: 6, length: 1, type: 'catTitle'}]},
            \   #{text: '   ELSE      stop 5 sec and can edit file',
            \     props: [#{col: 4, length: 4, type: 'catTitle'}]},
	        \   s:NoProp(''),
	        \   #{text: 'Now press  optionsNo  to start or  x  to exit',
            \     props: [#{col: 12, length: 8, type: 'catTitle'},
            \             #{col: 36, length: 1, type: 'catTitle'}]},
	        \ ], #{
	        \   filter: function('s:IntroFilter'),
	        \   callback: function('s:IntroClose'),
	        \   border: [],
	        \   padding: [],
	        \   mapping: 0,
	        \   drag: 1,
	        \   close: 'button',
	        \ })
    call s:IntroHighlight(0)
endfunc


func s:IntroFilter(id, key)
    if a:key == '1'
        call s:Clear()
        call s:CatWalk(g:CATSHILHOUETTE)
    elseif a:key == 'x' || a:key == 'X' || a:key == "\<Esc>"
        call s:Clear()
    endif
    return 1
endfunc


func s:IntroClose(id, res)
    call s:Clear()
endfunc


const s:introHL = [[s:start_title, 1],
                \  [s:start_title+2, 4],
                \  [s:start_title+7, 2],
                \  [s:start_title+10, 4],
                \  [s:start_title+15, 4],
                \  [s:start_title+20, 1],
                \  [s:start_title+22, 4]]
let s:intro_timer = 0
func s:IntroHighlight(idx)
    let idx = a:idx
    if idx >= len(s:introHL)
        let idx = 0
    endif
    let buf = winbufnr(s:intro)
    call prop_remove(#{type: 'introHL', bufnr: buf}, 1)
    call prop_add(1, s:introHL[idx][0],
            \     #{length: s:introHL[idx][1], bufnr: buf, type: 'introHL'})
    let s:intro_timer = timer_start(300, { -> s:IntroHighlight(idx + 1)})
endfunc
```
</div></details>

まずはメニューのメインである`s:Intro`関数以外の雑多な関数について紹介します。
`s:NoProp`関数はpopupウィンドウでプレーンテキストを出力するためのデコレート関数です。
`s:IntroFilter`関数はメニュー画面でのキーボード入力に対してどのような処理を行うかを決める関数です。引数`id`はpopupウィンドウのID、`key`は入力されたキーの情報です。関数内で引数を参照するときは`a:`と先頭につけて引数変数であることを明示する必要があります。
`s:IntroClose`関数はメニューウィンドウをウィンドウ右上の`X`ボタンで削除した場合の処理を記述します。ここでは単純にpopupウィンドウを削除しています。
`const s:introHL`はテキストに合わせたハイライト情報をリストで保持しています。
`s:IntroHighlight`関数はメニュー画面のタイトルテキストをハイライトするための関数です。ぶっちゃけなくても大丈夫です。

さて、メインの`s:Intro`関数についてですが、要はメニューをpopupウィンドウとして表示しています。そのpopupウィンドウについて詳細に見ていきます。
[ドキュメント](https://vim-jp.org/vimdoc-ja/popup.html#popup_create-arguments)によると、`popup_create`関数は第一引数のテキストを表示するpopupウィンドウを作成します。第二引数にはオプションを入力しますが、今回は以下を使用しています。

- `filter`: キー入力のフィルターを指定する。
- `callback`: popupウィンドウを閉じた際の処理を指定する。
- `border`: popupウィンドウの上下左右の修飾幅を指定する。空のリストが指定された場合は全周を囲う形の修飾をします。
- `padding`: popupウィンドウの上下左右のパディング幅(空白)を指定する空のリストが指定された場合は全周に幅1のパディングを施します。
- `mapping`: キーマッピングを指定する。`filter`が指定されている場合はFalseとなるが、空のリストを渡すことで明示的にキーマッピングをオフにしています。
- `drag`: `border`が指定されている場合は、これをTrueにするとそれを掴んでドラッグして移動させることができます。
- `close`: `button`、`click`、`none`を指定できます。`button`だとpopup右上に`X`ボタンを表示します。`click`ではpopupウィンドウのどこかをクリックすると閉じます。

第一引数のテキスト指定方法についてですが、行ごとに辞書型を利用して`text`と`prop`(プロパティ)を指定します。ちなみにプロパティを指定しない場合はテキストだけでも大丈夫です。

<details><summary>猫が走る部分</summary><div>

```autoload/withcat.vim
func s:CatWalk(walker)
    call s:Clear()
    " Init
    let s:popUpWindow = popup_create('', #{
            \ line: 1,
            \ col: 1,
            \ })
    let s:status = 1
    "call win_execute(s:popUpWindow ,'setlocal filetype=withcatHL')

    while s:status
        for i in range(len(a:walker))
            call popup_settext(s:popUpWindow, a:walker[i])
            redraw
            if getchar(0)
                let s:status = 0
                break
            endif
            sleep 100m
        endfor
    endwhile

    "call popup_close(s:popUpWindow)
    "call s:CatWalk()
    if getchar(0) == "q"
        call s:Clear()
        let s:popUpWindow = popup_create([
                \   #{text: 'Good Vim!',
                \     props: [#{col: 1, length: 9, type: 'catTitle'}]},
                \ ], #{
	            \   border: [],
	            \   padding: [],
	            \   mapping: 0,
	            \   drag: 1,
	            \   close: 'button',
                \ })
        call timer_start(2000, { -> s:Clear() })
    else
        call timer_start(5000, { -> s:CatWalk(a:walker) })
    endif
endfunc
```
</div></details>

そして本プラグインの目標、猫が走る部分です。ここのコードはkato-kさんの[Nyancat](https://github.com/kato-k/nyancat.vim)というプラグインをもとに作成しました。こちらも猫が走ります！しかもカラーリングも完備で素晴らしい完成度です。
まずは描画用のpopupウィンドウを`line=1,col=1`つまり左上に作成します。ここではまだテキストを表示する必要はありませんので空文字を第一引数に渡しています。
その後の`while`ループで猫を走らせています。`getchar`関数を利用することで入力があればbreakするようになっています。
最後は入力された文字によっての場合分けを行います。`q`が入力された場合は猫が走るのを完全にやめます。
それ以外が入力された場合は`timer_start`関数を利用して5秒待機後に`s:CatWalk`関数を再度呼び出しています。第二引数の文法については正直よくわかっていません...killersheepの方での使われ方を参考にしていますが...
待機中は一応普通にファイルを編集できますので、猫と戯れながら作業できます！


# おわりに
2021/8/4現在ではただ猫が走るだけのスクリプトです。猫と戯れるといいながらただ走っている場面を見続けるだけになっています。期待外れでしたら申し訳ありません🙇‍♂️
今後非同期処理を勉強してpopupを表示、猫を走らせたままファイル編集ができないか勉強しようと思います。
非同期処理ができるようなら、ファイル編集のキー入力を拾って猫の挙動を操作したりとかできたらなぁと願望もあったり...
もっと色々な動作も追加していきたいですし、やることが多くて結構です。