
vimとObsidianでデイリーメモを(なんかいい感じに)管理する
この記事はvim駅伝 4/22 の記事です。
だいたいの方は初めまして、ゲームと音楽とvimを愛する自称17歳です。
……どうしたー?声が小さいぞー?
vimrc、弄り倒していますか?最近は個人的なカスタムは出来る限り抑えてて、出来る限りvimの機能の中で自分が何とかしたいことを実現するように心がけています。それはさておき、一度vimに慣れてしまうと手元にあるツールは全てモーダルに書けないとストレスを覚えてしまうのがvimmerという生き物です。
なぜObsidianにすべきか?
本線の話にフォーカスを始めます。日々の業務のメモ、どうしていますか?当然モーダルに書きたいですよね?というわけで僕の業務メモの取り方なんですけど、まずメモツールとしてはObsidianを活用しています。
Obsidianで書いたメモはディレクトリ/ファイル名がタイトルのプレーンテキストとして(原則)管理されています。これはメモツールとしてのI/Fがありながら、裏道として好きなテキストエディタが使えるという事でもあり、そうやってObsidianのI/FとvimのI/Fを都合よく使い分けられることがObsidianを使っている一番の理由です。もう少し突っ込むと、マルチプラットフォームのツールなので、モバイルからはObsidianのI/Fで、PCからはObsidianとvimのI/Fを併用して使っています。同期の方法については長くなるので割愛しますが、いい感じに出来ます。
で、Obsidianはプラグインによる機能拡張があり、現在使っているCalenderプラグインではカレンダーベースのI/Fが提供され、それぞれのデイリーメモが日付に依存したファイル名で管理されています。勿論ファイル名さえ合っていれば外部からデイリーメモのファイルを作成してもokということになります。
これ、vimからもいい感じにファイル開いたり作ったり出来ないかなぁ、
なんて考えました。
例えばページめくるみたいに日付たどりながらファイル開いていけたらなぁ、とか。
というわけで今回のゴールは「日付がファイル名のバッファを手早く作りたい」と言う事になります。「えっただ作ればいいじゃん」という話に見えますが、今開いてるメモに対する前の日/次の日が月末/月初で、月の大小やうるう年の確認が必要な場合もあるわけで……
そうだね、vimscriptだね。
vimscript
scriptencoding utf-8
function s:delta(date, delta, format = '%Y/%m/%d') abort
python3 << EOF
import datetime
import vim
aDate = vim.eval("a:date")
aDelta = int(vim.eval("a:delta"))
aFormat = vim.eval("a:format")
date = datetime.datetime.strptime(aDate, aFormat)
delta = datetime.timedelta(days=aDelta)
dateTo = date + delta
vim.command("let dateTo =" + dateTo.strftime(aFormat))
EOF
return dateTo
endfunction
function! dailyMover#load() abort
lcd %:h
let g:dailyMoverLs = glob('*.md', 0, 1)->sort()
if index(g:dailyMoverLs, expand('%:t')) < 0
call add(g:dailyMoverLs, expand('%:t'))->sort()
endif
nnoremap <buffer> <silent> [d :<C-u>call dailyMover#moveRelative(v:count == 0 ? -1 : -v:count)<CR>
nnoremap <buffer> <silent> ]d :<C-u>call dailyMover#moveRelative(v:count == 0 ? 1 : v:count)<CR>
nnoremap <buffer> <silent> [D :<C-u>call dailyMover#moveAbsolute(v:count == 0 ? -1 : -v:count)<CR>
nnoremap <buffer> <silent> ]D :<C-u>call dailyMover#moveAbsolute(v:count == 0 ? 1 : v:count)<CR>
nnoremap <buffer> <silent> [<C-d> :<C-u>call dailyMover#open(g:dailyMoverLs[0])<CR>
nnoremap <buffer> <silent> ]<C-d> :<C-u>call dailyMover#open(g:dailyMoverLs[-1])<CR>
endfunction
function! dailyMover#moveRelative(delta) abort
let b:currentMemo = index(g:dailyMoverLs, expand('%:t'))
try
let openTo = g:dailyMoverLs[b:currentMemo+a:delta]
catch /^Vim\%((\a\+)\)\=:E684:/
let openTo = s:delta(split(g:dailyMoverLs[-1], '\.')[0], 1, '%y%m%d')..'.md'
endtry
call dailyMover#open(openTo)
endfunction
function! dailyMover#moveAbsolute(delta) abort
let openTo = s:delta(expand('%:t:r'), a:delta, '%y%m%d')..'.md'
call dailyMover#open(openTo)
endfunction
function! dailyMover#open(file) abort
try
call printf('edit %s', a:file)->execute()
catch /^Vim\%((\a\+)\)\=:E37:/
call printf('split %s', a:file)->execute()
endtry
endfunction
すいませんnoteってvimscriptのハイライト曖昧っぽいので見づらいかも。
こちらからだとgitのハイライトで見やすくなると思います。
方針
まず前提として、僕のvimrcの設定では既に":e today="みたいな方法で今日のメモが開くようになっています。
なってほしい挙動は、開いたmarkdownファイルがObsidianのデイリーメモだった場合、正確にはデイリーメモのディレクトリ内のファイルだった場合にftpluginの設定で一連のスクリプトを読み込んで、特定のキーマップで前日の、ないし次の日のファイルをどんどん開きたいです。一応キーマップは直交したいのでいい感じの空きをさがしたところ、"[d"ないし"]d"みたいなキーマップが適役かなと思いました。本来はインクルードファイル回りのジャンプにマップされていますが、どうせ特定のmarkdownファイルに対してのみ機能してほしいので、キーマップ定義時に<buffer>とかしてあげれば問題は起こらないということにしました。
date型
では実装していくんですけど、じゃあvimで日付ってどう扱えばいいんでしょう?幸いにvim自体が日付と文字列の変換を補助するstrftime()、strptime()関数を持っているのですが、残念ながらstrptime()関数がシェルに依存した関数なのでwindowsでは機能しません。というわけでstrptime()に相当するものを用意する必要があるので……そうだね、pythonだね。
スクリプト自体は超コンパクトなのでインラインで書いてしまいました、読めばわかりますよね?程度のものですが、python側にvimとオブジェクトのやり取りをするモジュールがある事だけはvim活として触れておきます。実際に今回使っているメソッドはvim.eval()でvim側の変数を読み込んで、vim.command()からvim側に返り値を渡しているだけですが。一応の話として、vim.evalでvim側の変数を読み込んだ場合、全て文字列として扱われているようなので必要に応じて型変換をしています。
頑張ってあらゆる状況に対応できるような雰囲気の関数ですが、実際は明日のメモを開くために1日インクリメントするためだけに使っています。ちくしょうどうしてこんなことに。
実際どう?
じゃあ実際の事考えます……実際って何の話かって話なんですけどそれは追々。
dailyMover#load()関数を使ってデイリーメモのファイルリストをグローバル変数として確保しており、基本的にはdailyMover#moveRelative()で開くファイルの決定はこのファイルリストを使っています。実際に紙のメモを漁るときの次のページ/前のページのような挙動を期待したもので、もし次のページ相当のファイルがない場合は最後のページ相当のバッファを開きます。この挙動がベストなので、日付に関する関数は作ったけど実際にやらせていることはただのインクリメント、ということになってしまったわけです。
他に……実際にそう使うことがなさそうなんですけど、リストとは無関係にn日先のファイル相当のバッファを開けるようにしたり、先頭/最終のページを開けるようなキーマップ/関数も用意してあります。これ以外のことが起こってしまう場合……例えば上司なりクライアントなりから「今月の14日って〇〇に行ってない?」みたいなことを聞かれた場合は直にファイル開いてしまえばいいので。
結論
結局何が言いたいの?って話なんですけど、多分僕がvimスクリプトを書く理由って「治具」を作る感覚に近いんだと思います。この記事を通して言いたいことも実際にはそれで、プラグインを使ってどんどん便利にしていこうぜ、というより自分が欲しいハンディな機能にフォーカスしたスクリプトを書いて、コンパクトに付き合って行けるんだぜ、ということです。というわけでスクリプト書いてるのに技術的な話にはほとんど触れていない、何の話がしたいのか分からないものが出来てしまったんですけど。まぁ治具ってそういうものなんだと思います。
それはそれとしてvimとObsidianの相性は抜群なので、こういうツールがあって、vimと併用したらいいとこどりで使えますよという話もしたかったんです。コミュニティプラグインも併用すればObsidianから直にvimでファイルを開かせて、モーダルにガリガリ書いて、保存すればモバイル版まで含めて勝手に反映されているというのはそれだけで魅力的です。魅力的すぎるので更によく連動するようにスクリプト書いたほどなので相当です。
是非Obsidianの方にも触れてみて、vimとの相性の良さを体感していただけたらな、と思います。