tmuxで~/.tmux.confに書けるオプションを足す方法

前書き

以前色々とpatchを書いたが、環境によって使い分けられるように~/.tmux.confで制御しようと思った
のでoption関連のコードリーディングした
したのはいいけどソースコードからしか情報とってないのでtmuxを書いてる人たちがどういった規格でコードを書いてるのか知りません

本文

改変する必要があるのはoptions-table.c
まず追加する先の選択肢として3種類ある
ServerとSessionとWindowといった構造体
それぞれの使い分けはServerとSessionはset-option
Windowはset-window-optionで設定できる
Serverは常にグローバル変数を使っている
SessionとWindowは設定で引数に-gを使ったらグローバル変数を使うようになってるが指定しなくても挙動に変化でないしどうしたいのかわからん(俺のtmuxがおかしいのか?)

次にその構造体にどう書くのか例を書く

const char *options_table_status_keys_list[] = {    //のちに登場するtypeがOPTIONS_TABLE_CHOICEのための選択肢
    "emacs", "vi", NULL
};

//ここではsessionを例に使う
const struct options_table_entry session_options_table[] = {
    { .name = "default-command",            //set-option ここの部分
      .type = OPTIONS_TABLE_STRING,         //オプションの値を文字列として扱う
      .default_str = ""                     //初期のこのオプションに対する値
    },

    { .name = "assume-paste-time",
      .type = OPTIONS_TABLE_NUMBER,         //オプションの値を数値として扱う
      .minimum = 0,                         //数値の最低値
      .maximum = INT_MAX,                   //数値の最大値
      .default_num = 1,                     //初期のこのオプションに対する値
    },

    { .name = "prefix",
      .type = OPTIONS_TABLE_KEY,            //オプションの値をキー入力として扱う
      .default_num = '\002',                //初期の値、指定できるやつはkey-string.c見たらわかる
    },

    { .name = "status-bg",                  //backgroundとして扱うなら.nameの後ろに-bgを入れないといけない
      .type = OPTIONS_TABLE_COLOUR,         //オプションの値をblackとかredみたいな色として扱う
      .default_num = 2,                     //初期の値、この場合green、指定できるやつはcolour.cを見たらわかる
      .style = "status-style"               //この色を適用したいstyleを指定する
    },

    { .name = "status-attr",                //attributesとして扱うなら.nameの後ろに-attrを入れないといけない
      .type = OPTIONS_TABLE_ATTRIBUTES,     //オプションの値をbrightとかboldみたいな文字の表現として扱う
      .default_num = 0,                     //初期の値、この場合none、指定できるやつはattributes.cを見たらわかる
      .style = "status-style"               //この表現を適用したいstyleを指定する
    },

    { .name = "status",
      .type = OPTIONS_TABLE_FLAG,           //オプションの値を0か1のみ使用してフラグとして使う
      .default_num = 1                      //初期の値、この場合、on、指定できるやつは0 off no 1 on yes cmd-set-option.cを見たらわかる
    },

    { .name = "status-keys",
      .type = OPTIONS_TABLE_CHOICE,         //オプションの値を定義したリストの中から選べる
      .choices = options_table_status_keys_list, //定義した文字列構造体の変数名を指定する
      .default_num = MODEKEY_EMACS          //初期の値、例に書いてるのはtmux.hで定義されてる、0
    },

    { .name = "status-style",
      .type = OPTIONS_TABLE_STYLE,          //オプションの値としてstatus-bgとかstatus-attrをまとめて書ける
      .default_str = "bg=green,fg=black"    //初期の値、style.cで式が評価される
    }

    { .name = NULL }                        //終わりを示すためにNULL
};

こんな感じのをかけたら後は
typeがOPTIONS_TABLE_STRINGの場合

char * options_get_string(struct options *oo, const char *name)

typeがOPTIONS_TABLE_STYLEの場合

struct grid_cell * options_get_style(struct options *oo, const char *name)

typeがそれ以外の場合

long long options_get_number(struct options *oo, const char *name)

で取得が可能です

書くためにやった手順(tmux-1.9a)

ソースコードを展開してそのディレクトリ内を~/.tmux.confに書くオプション名で検索する
ここではdefault-terminalを採用

% find ./tmux-1.9a -type f -exec grep -nH "default-terminal" {} +
./tmux-1.9a/examples/tmux.vim:177:      \ default-terminal
./tmux-1.9a/tmux.1:2227:.It Ic default-terminal Ar terminal
./tmux-1.9a/options-table.c:133:        { .name = "default-terminal",
./tmux-1.9a/server-fn.c:39:             term = options_get_string(&s->options, "default-terminal");
./tmux-1.9a/FAQ:183:    set -g default-terminal "screen-256color"
./tmux-1.9a/FAQ:378:    set -g default-terminal "screen-it"

この中で関係がありそうなものは

./tmux-1.9a/options-table.c:133:        { .name = "default-terminal",
./tmux-1.9a/server-fn.c:39:             term = options_get_string(&s->options, "default-terminal");

の2つになります。ファイル名から推測するに

./tmux-1.9a/options-table.c:133:        { .name = "default-terminal",

これでオプションを定義して

./tmux-1.9a/server-fn.c:39:             term = options_get_string(&s->options, "default-terminal");

これで定義しているように見えるのでoptions-table.cから先に見てみることにする

するとこのファイルの中ではオプションがいくつか分けられていて

/* Server options. */
const struct options_table_entry server_options_table[] = {

/* Session options. */
const struct options_table_entry session_options_table[] = {

/* Window options. */
const struct options_table_entry window_options_table[] = {

の3種類のオプションに分かれていることがわかる。この中から追加したいオプションがこのカテゴリに含まれるのかを考えて足す必要がありそう
とりあえずdefault-terminalを見ることにする

    { .name = "default-terminal",
        .type = OPTIONS_TABLE_STRING,
        .default_str = "screen"
    },

なるほど・・・わからんということでこの型を定義しているであろうincludeしているtmux.hを見る

extern const struct options_table_entry session_options_table[];

なのでoptions_table_entry型

struct options_table_entry {
    const char              *name;
    enum options_table_type type;

    u_int                   minimum;
    u_int                   maximum;
    const char              **choices;

    const char              *default_str;
    long long               default_num;

    const char              *style;
};

に新たに出てきたoptions_table_typeの定義を見る

enum options_table_type {
    OPTIONS_TABLE_STRING,
    OPTIONS_TABLE_NUMBER,
    OPTIONS_TABLE_KEY,
    OPTIONS_TABLE_COLOUR,
    OPTIONS_TABLE_ATTRIBUTES,
    OPTIONS_TABLE_FLAG,
    OPTIONS_TABLE_CHOICE,
    OPTIONS_TABLE_STYLE
};

なるほど・・・だいたい定義の仕方はわかったので次に読み取る関数の options_get_string(); を使っているところを調べる

% find ./tmux-1.9a -type f -exec grep -nH "options_get_string" {} +
./tmux-1.9a/window-copy.c:580:              options_get_string(&sess->options, "word-separators");
./tmux-1.9a/window-copy.c:586:              options_get_string(&sess->options, "word-separators");
./tmux-1.9a/window-copy.c:596:              options_get_string(&sess->options, "word-separators");
./tmux-1.9a/session.c:247:      shell = options_get_string(&s->options, "default-shell");
./tmux-1.9a/server-window.c:208:        ptr = options_get_string(&w->options, "monitor-content");
./tmux-1.9a/cmd-attach-session.c:170:           update = options_get_string(&s->options, "update-environment");
./tmux-1.9a/cmd-new-session.c:190:              cmd = options_get_string(&global_s_options, "default-command");
./tmux-1.9a/cmd-new-session.c:194:      update = options_get_string(&global_s_options, "update-environment");
./tmux-1.9a/cmd-split-window.c:83:              cmd = options_get_string(&s->options, "default-command");
./tmux-1.9a/cmd-split-window.c:141:     shell = options_get_string(&s->options, "default-shell");
./tmux-1.9a/server-client.c:129:        overrides = options_get_string(oo, "terminal-overrides");
./tmux-1.9a/server-client.c:777:        template = options_get_string(&s->options, "set-titles-string");
./tmux-1.9a/server-client.c:1054:       shell = options_get_string(&global_s_options, "default-shell");
./tmux-1.9a/status.c:88:            NULL, NULL, options_get_string(&s->options, "status-left"), t, 1);
./tmux-1.9a/status.c:109:           NULL, NULL, options_get_string(&s->options, "status-right"), t, 1);
./tmux-1.9a/status.c:211:               sep = options_get_string(oo, "window-status-separator");
./tmux-1.9a/status.c:226:               sep = options_get_string(oo, "window-status-separator");
./tmux-1.9a/status.c:634:       fmt = options_get_string(oo, "window-status-format");
./tmux-1.9a/status.c:637:               fmt = options_get_string(oo, "window-status-current-format");
./tmux-1.9a/status.c:1045:              wsep = options_get_string(oo, "word-separators");
./tmux-1.9a/status.c:1078:                      wsep = options_get_string(oo, "word-separators");
./tmux-1.9a/status.c:1101:                      wsep = options_get_string(oo, "word-separators");
./tmux-1.9a/status.c:1129:                      wsep = options_get_string(oo, "word-separators");
./tmux-1.9a/cmd-set-option.c:347:               oldval = options_get_string(oo, oe->name);
./tmux-1.9a/options.c:124:options_get_string(struct options *oo, const char *name)
./tmux-1.9a/tmux.h:1579:char   *options_get_string(struct options *, const char *);
./tmux-1.9a/cmd-new-window.c:86:                cmd = options_get_string(&s->options, "default-command");
./tmux-1.9a/names.c:86: fmt = options_get_string(&w->options, "automatic-rename-format");
./tmux-1.9a/server-fn.c:39:             term = options_get_string(&s->options, "default-terminal");
./tmux-1.9a/server-fn.c:246:    cmd = options_get_string(&c->session->options, "lock-command");

オプションの数に対して関数使っているところが少ないので別の関数もある予感
定義しているっぽい

./tmux-1.9a/options.c:124:options_get_string(struct options *oo, const char *name)

を見に行く

    char *
options_get_string(struct options *oo, const char *name)
{
    struct options_entry    o;

    if ((o = options_find(oo, name)) == NULL)
        fatalx("missing option");
    if (o->type != OPTIONS_STRING)
        fatalx("option not a string");
    return (o->str);
}

こんな定義になってる
関数名だけで判断するなら最初のif文でそのオプションが存在するのか
次のif文でそのオプションの値は文字列なのかどうかを見ているっぽい
他にも似たやつで

    long long
options_get_number(struct options *oo, const char *name)
{
    struct options_entry    *o;

    if ((o = options_find(oo, name)) == NULL)
        fatalx("missing option");
    if (o->type != OPTIONS_NUMBER)
        fatalx("option not a number");
    return (o->num);
}
    struct grid_cell *
options_get_style(struct options *oo, const char *name)
{
    struct options_entry    *o;

    if ((o = options_find(oo, name)) == NULL)
        fatalx("missing option");
    if (o->type != OPTIONS_STYLE)
        fatalx("option not a style");
    return (&o->style);
}

こんなのがある

色々ソースコードを追ってみたけど、変更する箇所は
options-table.cだけで良さげ
この中で追加するために考える必要がありそうなのは

/* Server options. */
const struct options_table_entry server_options_table[] = {

/* Session options. */
const struct options_table_entry session_options_table[] = {

/* Window options. */
const struct options_table_entry window_options_table[] = {

この中のどのジャンルに含まれるかを考える
中のオプションを見たとき
~/.tmux.confに書くとき
Server optionsはset-optionで
Session optionsはset-optionで
Window optionsはset-window-optionで書くことがわかる
じゃあServerとSessionの違いは?ということで調べたらそれぞれ取得するときの第1引数に使うものが異なってくる
Server optionsの場合

options_get_number(&global_options, "buffer-limit");

Session optionsの場合

options_get_string(&s->options, "default-terminal");

というように&global_optionsと&s->optionsで使い分けている
同じようにwindowは&w->options
global_optionsはtmux.cでoptions型のglobal変数として定義されている
s->optionsは

void
server_fill_environ(struct session *s, struct environ *env)
{
    char    var[MAXPATHLEN], *term;
    u_int   idx;
    long    pid;

    if (s != NULL) {
        term = options_get_string(&s->options, "default-terminal");
        environ_set(env, "TERM", term);

        idx = s->id;
    } else
        idx = -1;
    pid = getpid();
    xsnprintf(var, sizeof var, "%s,%ld,%d", socket_path, pid, idx);
    environ_set(env, "TMUX", var);
}

で書いてる通りsession型、定義はtmux.h

struct session {
    u_int        id;

    char        *name;
    int      cwd;

    struct timeval   creation_time;
    struct timeval   activity_time;
    struct timeval   last_activity_time;

    u_int        sx;
    u_int        sy;

    struct winlink  *curw;
    struct winlink_stack lastw;
    struct winlinks  windows;

    struct options   options;

#define SESSION_UNATTACHED 0x1  /* not attached to any clients */
    int      flags;

    struct termios  *tio;

    struct environ   environ;

    int      references;

    TAILQ_ENTRY(session) gentry;
    RB_ENTRY(session)    entry;
};

s->optionsがどうやって作られてるのか調べるために関数を呼び出した関数を呼び出した…と遡っていくと
session.cにて

options_init(&s->options, &global_s_options);

global_s_options変数から生成されているっぽい
これもtmux.cでoptions型のglobal変数として定義されている
同じようにwindow用のglobal_w_options変数も定義されている
これの初期化はtmux.cの

    options_init(&global_options, NULL);
    options_table_populate_tree(server_options_table, &global_options);
    options_set_number(&global_options, "quiet", quiet);

    options_init(&global_s_options, NULL);
    options_table_populate_tree(session_options_table, &global_s_options);
    options_set_string(&global_s_options, "default-shell", "%s", getshell());

    options_init(&global_w_options, NULL);
    options_table_populate_tree(window_options_table, &global_w_options);

そしてsession.cやwindow.cで新しく作られたときに

    options_init(&s->options, &global_s_options);

    options_init(&w->options, &global_w_options);

のようにそれぞれのセッションやウィンドウのoptionの設定に反映させているのがわかる

では初期化以外でoptionの設定をいじる場合どこでいじるのかということで
options_set_*を使用している初期化以外の関数を見たらcmd-set-option.cに
cmd_set_option_*という法則で関数名が定義されていた
さらにそれを追ったらcmd_set_option_exec関数にたどり着いた

enum cmd_retval
cmd_set_option_exec(struct cmd *self, struct cmd_q *cmdq)
{
    struct args             *args = self->args;
    const struct options_table_entry    *table, *oe;
    struct session              *s;
    struct winlink              *wl;
    struct client               *c;
    struct options              *oo;
    struct window               *w;
    const char              *optstr, *valstr;
    u_int                    i;

    /* Get the option name and value. */
    optstr = args->argv[0];
    if (*optstr == '\0') {
        cmdq_error(cmdq, "invalid option");
        return (CMD_RETURN_ERROR);
    }
    if (args->argc < 2)
        valstr = NULL;
    else
        valstr = args->argv[1];

    /* Is this a user option? */
    if (*optstr == '@')
        return (cmd_set_option_user(self, cmdq, optstr, valstr));

    /* Find the option entry, try each table. */
    table = oe = NULL;
    if (options_table_find(optstr, &table, &oe) != 0) {
        cmdq_error(cmdq, "ambiguous option: %s", optstr);
        return (CMD_RETURN_ERROR);
    }
    if (oe == NULL) {
        cmdq_error(cmdq, "unknown option: %s", optstr);
        return (CMD_RETURN_ERROR);
    }

    /* Work out the tree from the table. */
    if (table == server_options_table)
        oo = &global_options;
    else if (table == window_options_table) {
        if (args_has(self->args, 'g'))
            oo = &global_w_options;
        else {
            wl = cmd_find_window(cmdq, args_get(args, 't'), NULL);
            if (wl == NULL) {
                cmdq_error(cmdq,
                    "couldn't set '%s'%s", optstr,
                    (!args_has(args, 't') && !args_has(args,
                    'g')) ? " need target window or -g" : "");
                return (CMD_RETURN_ERROR);
            }
            oo = &wl->window->options;
        }
    } else if (table == session_options_table) {
        if (args_has(self->args, 'g'))
            oo = &global_s_options;
        else {
            s = cmd_find_session(cmdq, args_get(args, 't'), 0);
            if (s == NULL) {
                cmdq_error(cmdq,
                    "couldn't set '%s'%s", optstr,
                    (!args_has(args, 't') && !args_has(args,
                    'g')) ? " need target session or -g" : "");
                return (CMD_RETURN_ERROR);
            }
            oo = &s->options;
        }
    } else {
        cmdq_error(cmdq, "unknown table");
        return (CMD_RETURN_ERROR);
    }

    /* Unset or set the option. */
    if (args_has(args, 'u')) {
        if (cmd_set_option_unset(self, cmdq, oe, oo, valstr) != 0)
            return (CMD_RETURN_ERROR);
    } else {
        if (args_has(args, 'o') && options_find1(oo, optstr) != NULL) {
            if (!args_has(args, 'q'))
                cmdq_print(cmdq, "already set: %s", optstr);
            return (CMD_RETURN_NORMAL);
        }
        if (cmd_set_option_set(self, cmdq, oe, oo, valstr) != 0)
            return (CMD_RETURN_ERROR);
    }

    /* Start or stop timers when automatic-rename changed. */
    if (strcmp(oe->name, "automatic-rename") == 0) {
        for (i = 0; i < ARRAY_LENGTH(&windows); i++) {
            if ((w = ARRAY_ITEM(&windows, i)) == NULL)
                continue;
            if (options_get_number(&w->options, "automatic-rename"))
                queue_window_name(w);
            else if (event_initialized(&w->name_timer))
                evtimer_del(&w->name_timer);
        }
    }

    /* Update sizes and redraw. May not need it but meh. */
    recalculate_sizes();
    for (i = 0; i < ARRAY_LENGTH(&clients); i++) {
        c = ARRAY_ITEM(&clients, i);
        if (c != NULL && c->session != NULL)
            server_redraw_client(c);
    }

    return (CMD_RETURN_NORMAL);
}

引数に使われるoption型はオプション名でServerかWindowかSessionのオプションかを判別して
WindowまたはSessionの場合は引数に-tがあればその指定した奴だけ変更するようになっている
そこから考えるに

/* Server options. */
const struct options_table_entry server_options_table[] = {

/* Session options. */
const struct options_table_entry session_options_table[] = {

/* Window options. */
const struct options_table_entry window_options_table[] = {

Serverはtmux全体でSessionとかWindowの概念とかそんなもの使わない奴に使って
Sessionはセッションごと
Windowはウィンドウごとの定義に使うみたい (正直最初から薄々気づいてた)

次にこれの使い分けについて調べる

enum options_table_type {
    OPTIONS_TABLE_STRING,
    OPTIONS_TABLE_NUMBER,
    OPTIONS_TABLE_KEY,
    OPTIONS_TABLE_COLOUR,
    OPTIONS_TABLE_ATTRIBUTES,
    OPTIONS_TABLE_FLAG,
    OPTIONS_TABLE_CHOICE,
    OPTIONS_TABLE_STYLE
};
>||
% find ./tmux-1.9a -type f -exec grep -nH "OPTIONS_TABLE_STRING" {} +
./tmux-1.9a/cmd-set-option.c:299:       case OPTIONS_TABLE_STRING:
(中略)
./tmux-1.9a/options-table.c:854:                case OPTIONS_TABLE_STRING:
./tmux-1.9a/options-table.c:877:        case OPTIONS_TABLE_STRING:

この中で1個ずつ確認して一番怪しい初期化の時に呼び出している

void
options_table_populate_tree(
    const struct options_table_entry *table, struct options *oo)
{
    const struct options_table_entry    *oe;

    for (oe = table; oe->name != NULL; oe++) {
        switch (oe->type) {
        case OPTIONS_TABLE_STRING:
            options_set_string(oo, oe->name, "%s", oe->default_str);
            break;
        case OPTIONS_TABLE_STYLE:
            options_set_style(oo, oe->name, oe->default_str, 0);
            break;
        default:
            options_set_number(oo, oe->name, oe->default_num);
            break;
        }
    }
}

を見るに設定を書き込む方法が3種類で実現されている
それぞれの関数はoptions.cで定義されているので見ると

struct options_entry *printflike3
options_set_string(struct options *oo, const char *name, const char *fmt, ...)

がnameの文字列を代入

struct options_entry *
options_set_number(struct options *oo, const char *name, long long value)

valueの値を代入

struct options_entry *
options_set_style(struct options *oo, const char *name, const char *value,
    int append)

style_parse関数で式を評価して正しければvalueの指定されている色を設定している

次にnameやvalueの数値が入力可能な数値か判定している関数があるので見る

/* Set an option. */
int
cmd_set_option_set(struct cmd *self, struct cmd_q *cmdq,
    const struct options_table_entry *oe, struct options *oo, const char *value)

この関数から
cmd_set_option_( typle ) (self, cmdq, oe, oo, value);
みたいな形式の関数を呼び出している

やってることはだいたいこんな感じ

type 処理
string 引数にaがあれば前の値の後ろに付け足す
number 最低値と最大値の値が超えてないか
key キーを指定できてるか
colour blackとかredとか色を指定できているか
attributes brightとかboldとか文字の表現を指定できてるか
flag 空エンターならflagを逆に,onかyesかoffかnoになってるか
choice choices変数の構造体にある文字列かどうか
style 引数にaがあるか

colourは評価が終えた後styleに指定している所に色の設定を加えます
style_update_new関数の仕様上name変数は最後が-bgか-fgかじゃないと動きません
同じようにattributesは-attrじゃないと動きません

choiceに入る構造体は

const char *options_table_mode_keys_list[] = {
    "emacs", "vi", NULL
};

こんな感じで最後にNULLがあればおk

これで必要な情報が揃ったので実際に実装するとしたら
options-table.cにある

/* Server options. */
const struct options_table_entry server_options_table[] = {

/* Session options. */
const struct options_table_entry session_options_table[] = {

/* Window options. */
const struct options_table_entry window_options_table[] = {

この構造体の中から好きなものを選び
typeの場合によっては.choiceに入れる構造体や.styleを付け足せばオプションが追加できる
処理の中で取得するには

    char * options_get_string(struct options *oo, const char *name)
    long long options_get_number(struct options *oo, const char *name)
    struct grid_cell * options_get_style(struct options *oo, const char *name)

この関数を使って取得できる

後書き

http://mojavy.com/blog/2011/12/06/tmux_advent_calendar_2011/
書いてる途中で先駆者を見つけてしまったがまあいいか