3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-08-04

概要

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

リポジトリはこちら
どなたでもお気軽にコメント・プルリクいただけると嬉しいです。

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

cat_gif.gif

目次

WithCat

今回のプラグイン、WithCatはvimにpopup機能が追加された際に公開されたkillersheepをもとに作成しています。
ディレクトリ構成は以下の通りとなっています。

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

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

: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関数を呼び出してメニュー画面を開きます。

`plugin/cat_shilhouette`
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+                 ',
    \]
\]

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のコードのうち必要な部分だけをコピペしてきました。

メイン部分
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

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

メニュー
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

まずはメニューのメインである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ウィンドウについて詳細に見ていきます。
ドキュメントによると、popup_create関数は第一引数のテキストを表示するpopupウィンドウを作成します。第二引数にはオプションを入力しますが、今回は以下を使用しています。

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

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

猫が走る部分
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

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

おわりに

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

3
0
0

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
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?