概要
$$\huge{どうしてもvimで猫と戯れたい!}$$$$そう思ったことはありませんか?$$$$\large{ありますよね?}$$$$\huge{そう、あるんですよ。}$$
ということでそんなプラグインを目指して開発をしていこうと思い立ちました。
本記事はとりあえずのプロトタイプを作成したのでその覚書になります。
リポジトリはこちら
どなたでもお気軽にコメント・プルリクいただけると嬉しいです。
なお、このプラグインはvimのpopup機能を活用したものですので、popup機能が利用できるvimでご利用ください。
目次
WithCat
今回のプラグイン、WithCatはvimにpopup機能が追加された際に公開されたkillersheepをもとに作成しています。
ディレクトリ構成は以下の通りとなっています。
.
├── LICENSE
├── README.md
├── autoload
│ └── withcat.vim
└── plugin
├── cat_silhouette.vim
└── withcat.vim
使用方法は至って簡単で、vimを開いてノーマルモードにて
:WithCat
と入力すればOKです。
##plugin
ディレクトリ
まずはplugin
ディレクトリの中身から解説していきます。
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`
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
ディレクトリにはメインとなるスクリプトが入っています。
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のコードのうち必要な部分だけをコピペしてきました。
メイン部分
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
関数へと処理が移ります。
メニュー
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
:button
、click
、none
を指定できます。button
だとpopup右上にX
ボタンを表示します。click
ではpopupウィンドウのどこかをクリックすると閉じます。
第一引数のテキスト指定方法についてですが、行ごとに辞書型を利用してtext
とprop
(プロパティ)を指定します。ちなみにプロパティを指定しない場合はテキストだけでも大丈夫です。
猫が走る部分
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を表示、猫を走らせたままファイル編集ができないか勉強しようと思います。
非同期処理ができるようなら、ファイル編集のキー入力を拾って猫の挙動を操作したりとかできたらなぁと願望もあったり...
もっと色々な動作も追加していきたいですし、やることが多くて結構です。