muttの:sourceでファイル名も補完できるようにするpatchを書いた時のメモ
今回書いたmuttのpatch
https://raw.github.com/silenvx/PKGBUILD/master/mutt/source_complete.patch
ここにおいてますが、archlinuxユーザーなどのmakepkgが使える環境下の人は
% git clone https://github.com/silenvx/PKGBUILD.git
% cd PKGBUILD/mutt
% makepkg -iこれで楽にインストールができます
なんでこんなpatchを書いたのか
特定のファイルをsourceした後に、cキーの後に?キーを入力してメールのアカウントを切り替えられるようにしていて、そのsourceのファイル名を毎回打つのが煩わしくなったので。
この切り替えるやり方は私のmuttrcにコメントで書いてあります
今はhttps://github.com/silenvx/dotfiles/blob/master/misc/default/HOME/.muttrcここにおいてるはず
この記事でも簡単に書いておくと
.muttrcには
source ~/.mutt/.この後用意するアカウントごとのファイル名
このように、muttの起動時にログインしたいアカウントに関するファイルをsourceするように書いておいて
上記のアカウントごとのファイルの場所に
set my_account_name='@より前の部分'
source ~/.mutt/この後用意するメールサーバごとの設定ファイルが書かれたファイル名
set imap_pass='パスワード'
set smtp_pass='パスワード'このように書く。このアカウントごとのファイルを、アカウントを切り替える時にsourceします
次にメールサーバごとの設定ファイルを書きますが、gmailを例にすると
mailboxes imaps://imap.gmail.com/inbox
set hostname="gmail.com"
set folder='imaps://imap.gmail.com/'
set imap_user="${my_account_name}@gmail.com"
set spoolfile='+INBOX'
set sendmail=
set smtp_url="smtp://${my_account_name}@smtp.gmail.com:587/"
set realname="${my_account_name}@gmail.com"
set use_from="yes"
set from="${my_account_name}@gmail.com"
set postponed='+[Gmail]/下書き'
set record=だいたいこんな感じでできます。変数はシェルスクリプトのように${変数名}で表現ができるので後は書きたいように書いてください
ここより下はmuttのpatchを書いた時のメモというか思考というか手順みたいなもの (読む必要はありません)
まず始めに、補完がどのように実装されているか調べるために
muttのソースコードを落としてきて*1
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'create-alias' {} \;こんな感じで調べる。なぜcreate-aliasで絞るかというと
muttにてindexの画面で:execをした後に補完をするとcreate-aliasが出るからです
ちなみにgrepのオプションの-nは行数、-Hでファイル名が出るのでfindと組み合わせた場合に便利です
すると
mutt-1.5.21/functions.h:86: { "create-alias", OP_CREATE_ALIAS, "a" },
mutt-1.5.21/functions.h:177: { "create-alias", OP_CREATE_ALIAS, "a" },
mutt-1.5.21/functions.h:392: { "create-alias", OP_CREATE_ALIAS, "a" },
mutt-1.5.21/init.h:106: ** \fC$<create-alias>\fP function. Entries added to this file areこんな感じの結果が返ってきますのでmutt-1.5.21/functions.hを見ることにします
mutt-1.5.21/init.hはコメントなので今回は見ません
するとこれらの値は
85 struct binding_t OpMain[] = { /* map: index */
175 struct binding_t OpPager[] = { /* map: pager */
368 struct binding_t OpBrowser[] = { /* map: browser */に書いてあることがわかります。最初に書いてある数値は行数なので実際には書いてません。
今回はcreate-aliasで絞ったので、この3つしかヒットしませんでしたが、実際にはmap毎に書いてあるみたいです
今回はindexのところで補完を行ったのでとりあえず、85行目の
struct binding_t OpMain[] = { /* map: index */この構造体を使っている処理を探して、そこから考えていくとします。
というわけで最初と同じように絞ります
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'OpMain' {} \;
mutt-1.5.21/functions.h:85:struct binding_t OpMain[] = { /* map: index */
mutt-1.5.21/keymap.c:709: create_bindings (OpMain, MENU_MAIN);
mutt-1.5.21/keymap.c:928: return OpMain;
mutt-1.5.21/keymap.h:105:extern struct binding_t OpMain[];mutt-1.5.21/keymap.cにヒントが書かれていそうなので見ます
最初のcreate_bindings (OpMain, MENU_MAIN);は
702 void km_init (void)
703 {この関数の中にあって、この関数自体はキーバインドの初期化をしているようなので今回はあまり関係がなさそうなので無視します
なので注目すべきなのは return OpMain; これですね。
923 struct binding_t *km_get_table (int menu)
この関数で、戻り値として利用されていますので、さらにこの関数を使っている処理を探すことにしますので、再び絞ります
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'km_get_table' {} \;
mutt-1.5.21/keymap.c:368: struct binding_t *binding = km_get_table (Menus[i].value);
mutt-1.5.21/keymap.c:467: if ((bindings = km_get_table (menu)) &&
mutt-1.5.21/keymap.c:486: bindings = km_get_table (Menus[i].value);
mutt-1.5.21/keymap.c:923:struct binding_t *km_get_table (int menu)
mutt-1.5.21/keymap.c:1002: bindings = km_get_table (menu[i]);
mutt-1.5.21/keymap.c:1085: if ((bindings = km_get_table (CurrentMenu)) == NULL
mutt-1.5.21/help.c:47: if ((map = km_get_table(menu)))
mutt-1.5.21/help.c:345: funcs = km_get_table (menu);
mutt-1.5.21/init.c:2536: struct binding_t *menu = km_get_table (CurrentMenu);
mutt-1.5.21/keymap.h:101:struct binding_t *km_get_table (int menu);後は、それぞれの処理をしている関数名から何をしている処理なのかを予測していくと
mutt-1.5.21/init.cに補完関連らしき関数を見つけることができました
2430 int mutt_command_complete (char *buffer, size_t len, int pos, int numtabs)
名前的に考えて、恐らくこの関数で補完関連を司っているのでしょう。
注目すべきなのは
2534 else if (!mutt_strncmp (buffer, "exec", 4))
2535 {
2536 struct binding_t *menu = km_get_table (CurrentMenu); この部分ですね。どうやらmutt_strncmp関数で入力途中のコマンドがexecなのか判定できるのでしょう。
引数から考えて、bufferがその入力途中のコマンドが入っていて、"exec"が検索したい文字、最後の引数の4が何文字まで検索するか、なのでしょう。きっと。
念の為、この関数の処理を調べるとしますので、再びこのように絞ります
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'mutt_strncmp' {} \;
(中略)
mutt-1.5.21/lib.c:878:int mutt_strncmp(const char *a, const char *b, size_t l)というわけで、途中で略しましたが関数名の前に型があるのがきっと宣言しているところなのでmutt-1.5.21/lib.cを見ることにします
878 int mutt_strncmp(const char *a, const char *b, size_t l)
879 {
880 return strncmp(NONULL(a), NONULL(b), l);
881 }strncmpのラッパーのようなので、さっきの考え方はあっていたようです。
さて、問題はこのif文の中の処理がどうなっているか、です。
2534 else if (!mutt_strncmp (buffer, "exec", 4))
buffer変数に入力途中の文字列が入っているということはだいたい察しがつくので、この変数を書き換えている箇所を見ればよさそうです
そういうわけで、書き換えてるのは
2430 int mutt_command_complete (char *buffer, size_t len, int pos, int numtabs)
2432 char *pt = buffer;
2579 strncpy (pt, Completed, buffer + len - pt - spaces);この3行から考えるに
2579 strncpy (pt, Completed, buffer + len - pt - spaces);
この処理で、他の補完も同じようになっているのでこれを使えばよさそうです
補完したい文字はCompleted変数に書き込んでおけばいいみたいですね
さて、このCompletedの文字列がどこで書き換えられているか、というと
2572 if (numtabs == 1 && Num_matched == 2)
2573 snprintf(Completed, sizeof(Completed),"%s", Matches[0]);
2574 else if (numtabs > 1 && Num_matched > 2)
2575 /* cycle thru all the matches */
2576 snprintf(Completed, sizeof(Completed), "%s",
2577 Matches[(numtabs - 2) % Num_matched]);だいたいこんな感じで書き換えているみたいです
さて、このnumtabs変数とNum_matched変数はどのように生成されているかというと
numtabs変数はこの関数を呼び出した時の引数として使われていてmutt_command_completeの呼び出し元を追うと
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'mutt_command_complete' {} \;
mutt-1.5.21/protos.h:281:int mutt_command_complete (char *, size_t, int, int);
mutt-1.5.21/init.c:2430:int mutt_command_complete (char *buffer, size_t len, int pos, int numtabs)
mutt-1.5.21/enter.c:557: else if (!mutt_command_complete (buf, buflen, i, state->tabs))mutt-1.5.21/enter.cでmutt_command_complete関数を呼び出しており、
numtabsの値はstate->tabsに入っていることがわかるので、今度はこの変数の生成方法を追います
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'state->tabs' {} \;
mutt-1.5.21/enter.c:299: state->tabs = 0;
mutt-1.5.21/enter.c:488: state->tabs++;
mutt-1.5.21/enter.c:556: state->tabs = 0;
mutt-1.5.21/enter.c:557: else if (!mutt_command_complete (buf, buflen, i, state->tabs))
mutt-1.5.21/enter.c:640: state->tabs = 0;ヒットした5行のうち4行は値の初期化と関数の呼び出しに使われているので、残りのインクリメントをしているところを見ることにします
486 case OP_EDITOR_COMPLETE:
487 case OP_EDITOR_COMPLETE_QUERY:
488 state->tabs++;こうなっていたので、上にさかのぼってswitchを探すと
298 if (ch != OP_EDITOR_COMPLETE && ch != OP_EDITOR_COMPLETE_QUERY)
299 state->tabs = 0;
301 switch (ch)どうやらchの値がOP_EDITOR_COMPLETE(_QUERY)?であればインクリメントされ、そうでなければ初期化されるようです
では、このchの生成方法を調べますが変数名が短すぎて関係ないものが多数ヒットしてしまう恐れがあるので
この処理をしている関数を上から順に見ていきますと
222 int ch, w, r;
289 if ((ch = km_dokey (MENU_EDITOR)) == -1)ここで代入されていることがわかります
km_dokey関数は名前的に考えて、キー入力関連だと思うので定数らしきOP_EDITOR_COMPLETEについて調べるとします
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'OP_EDITOR_COMPLETE' {} \;
mutt-1.5.21/functions.h:416: { "complete", OP_EDITOR_COMPLETE, "\t" },
mutt-1.5.21/functions.h:417: { "complete-query", OP_EDITOR_COMPLETE_QUERY, "\024" },
(後略)どうやらascii文字で定義されているようです。補完に使うキーはtabキーで、それは'\t'なのでさっきの仮説はきっと合っていたのでしょう。
ここまでの情報を統合すると、タブを押して補完の結果を切り替えた回数がnumtabs変数に入ると考えられます
次に
Num_matched変数は
2383 static int Num_matched = 0; /* Number of matches for completion */
最初にこのようにグローバル変数でstatic変数として宣言されており
2501 if (numtabs == 1)
2502 {
2503 Num_matched = 0;1回目の補完時に必ず0に初期化されて
2512 Matches[Num_matched++] = User_typed;
先ほどと同じif文の中でインクリメントされてるので必ず1以上になりますが、それだけだと
2525 if (numtabs == 1 && Num_matched == 2)
2527 else if (numtabs > 1 && Num_matched > 2)これらのif文に入ることができないので他にNum_matched変数が変動する箇所を探します
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'Num_matched' {} \;
(前略)
mutt-1.5.21/init.c:2419: Matches[Num_matched++] = src;
(中略)
mutt-1.5.21/init.c:2456: Matches[Num_matched++] = User_typed;
(中略)
mutt-1.5.21/init.c:2512: Matches[Num_matched++] = User_typed;
(中略)
mutt-1.5.21/init.c:2559: Matches[Num_matched++] = User_typed;
(後略)変動していない、もしくは0を代入している箇所は省きました
これを見ると、1つだけ違うのがあって、それが怪しいのでまずそれを最初に見ます。
するとこの処理はこの関数で行われていることがわかります
2412 static void candidate (char *dest, char *try, const char *src, int len)
このcandidate関数はnumtabs変数が1の時、つまりは初めての補完の時に
2536 struct binding_t *menu = km_get_table (CurrentMenu);
2538 if (!menu && CurrentMenu != MENU_PAGER)
2539 menu = OpGeneric;
(中略)
2549 for (num = 0; menu[num].name; num++)
2550 candidate (Completed, User_typed, menu[num].name, sizeof (Completed));
2551 /* try the generic menu */
2552 if (Completed[0] == 0 && CurrentMenu != MENU_PAGER)_
2553 {
2554 menu = OpGeneric;
2555 for (num = 0; menu[num].name; num++)
2556 candidate (Completed, User_typed, menu[num].name, sizeof (Completed));
2557 }このようにfor文を使って、呼ばれているので2以上になることはありそうです
問題は、何回このfor文を繰り返すか、です
ではこのfor文を見ていきましょう
2549 for (num = 0; menu[num].name; num++)
これは、きっとmenu配列の最後に終わりのデータとして0が代入されているのでしょう。
このmenu配列はkm_get_table関数の戻り値でなので
最初の方に書いたようにmutt-1.5.21/functions.hにあるOp何とかかんとかの配列が代入されるのでしょう。
それらの配列の最後にはこのようなbinding_t構造体があって
{ NULL, 0, NULL }binding_t構造体のnameはどれなのか調べるために
% find mutt-1.5.21/ -type f -regex '.*\.\(c\|h\)' -exec grep -nH 'binding_t' {} \;
(前略)
mutt-1.5.21/keymap.h:94:struct binding_t
(後略)で、見てみると
94 struct binding_t
95 {
96 char *name; /* name of the function */
97 int op; /* function id number */
98 char *seq; /* default key binding */
99 };仮説は正しかったようです。
というわけでmenuの配列の数-1だけcandidate関数が呼び出されるようです
このcandidate関数の上にコメントがあるので訳します
補完を助けるための関数です
可能な補完を支援するためにdestバッファを変更します
dest = 補完の結果はここで取得
try = ユーザーが補完するために入力したデータ
src = 補完の候補
len = destバッファの長さらしいので、呼び出し元と照らし合わせます
2412 static void candidate (char *dest, char *try, const char *src, int len)
2550 candidate (Completed, User_typed, menu[num].name, sizeof (Completed));ここに出てくるUser_typedはこのように定義されており
2546 strfcpy (User_typed, pt, sizeof (User_typed));
ptのコピーのようなものみたいです。
それで、candidate関数の一部を書くと
tryの文字列がsrcにあったらMatches関数の領域を増やして、そこにsrcのアドレスがはいります
その時、Num_matched変数はインクリメントされるので少なくとも2以上にはなります
そういうわけで
2572 if (numtabs == 1 && Num_matched == 2)
2573 snprintf(Completed, sizeof(Completed),"%s", Matches[0]);
2574 else if (numtabs > 1 && Num_matched > 2)
2575 /* cycle thru all the matches */
2576 snprintf(Completed, sizeof(Completed), "%s",
2577 Matches[(numtabs - 2) % Num_matched]);候補が1つだけしかない場合は上のif文で、候補が複数あった場合は下のelse if文が呼び出されるみたいですね
else if文のこの計算式は
(numtabs - 2) % Num_matched
tabキーを一定回数以上押したら最初に戻るようになってるみたいです
というわけで補完の追加方法はわかったので後はどのようにファイル名を取得するかです
調べたところ、readdir関数を使えばいいみたいなのでそれを使用して実装するだけなので後は端折ります
後書き
goto文を使ったり若干、スパゲッティソースになった気がするので気が向いたら改良しますが今のところ特に不満はないのでしばらくはこのまま。
ただ、ファイル名にスペースがあれば区切りと勘違いされるバグがあるので、注意。そんなファイル名は使わないので今すぐは直しませんが、時間ができた時に直します。