前回は、ターミナルにとってはエスケープ(0x1b)から始まる一連の文字列には特別な意味があり、ターミナルが出力を受けるとそれに対応した機能を発動するということを説明した。今回は、このエスケースシーケンスを使ってどんな面白いことが実現されているのかを説明する。

kilo.cの320行目辺りに記述されているgetWindowSize()という関数を見てほしい。

ターミナルのサイズを取得するgetWindowSize()という関数

/* Try to get the number of columns in the current terminal. If the ioctl()

 * call fails the function will try to query the terminal itself.
 * Returns 0 on success, -1 on error. */
int getWindowSize(int ifd, int ofd, int *rows, int *cols) {
    struct winsize ws;

if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
    /* ioctl() failed. Try to query the terminal itself. */
    int orig_row, orig_col, retval;

    /* Get the initial position so we can restore it later. */
    retval = getCursorPosition(ifd,ofd,&orig_row,&orig_col);
    if (retval == -1) goto failed;

    /* Go to right/bottom margin and get position. */
    if (write(ofd,"\x1b[999C\x1b[999B",12) != 12) goto failed;
    retval = getCursorPosition(ifd,ofd,rows,cols);
    if (retval == -1) goto failed;

    /* Restore position. */
    char seq[32];
    snprintf(seq,32,"\x1b[%d;%dH",orig_row,orig_col);
    if (write(ofd,seq,strlen(seq)) == -1) {
        /* Can't recover... */
    }
    return 0;
} else {
    *cols = ws.ws_col;
    *rows = ws.ws_row;
    return 0;
}

failed:
    return -1;

C言語に慣れていない方はもうこのページを閉じようとしていると思う。もうちょっとだけ待ってほしい! まだいけるはずだ!

一見難しそうに見えるが、これは基本的に次のような処理をしているだけだ。

ioctl()の処理が成功したら、得られた値を取得

 ioctl(1, TIOCGWINSZ, &ws)の処理が成功
    ↓
*cols = ws.ws_col;
 *rows = ws.ws_row;

ioctl()の処理が失敗したら、エスケープシーケンスを発行

ioctl(1, TIOCGWINSZ, &ws)の処理が失敗
↓
if (write(ofd,"\x1b[999C\x1b[999B",12) != 12) goto failed;
retval = getCursorPosition(ifd,ofd,rows,cols);

ioctl(1, TIOCGWINSZ, &ws)というのはウィンドウサイズ(ターミナルサイズ)を取得する処理を行っているだけだ。この処理がうまくいくと、winsizeという構造体として用意したwsという変数に値が入ることになる。この部分の処理だけを抜き出して実装すると次のようになる。

getwinsize.c: ioctl(2)システムコールを使ってウィンドウサイズ(ターミナルサイズ)を取得するコード

#include
#include 
#include 

int
main(int argc, char *argv[])
{
    struct winsize ws;

/*
 * ioctl(2)システムコールを使ってウィンドウサイズ
 * (ターミナルサイズ)を取得し、構造体winsize wsに
 * 値を保存している。
 */
ioctl(1, TIOCGWINSZ, &ws);

printf("ws_row: %d, ws_col: %d\n", ws.ws_row, ws.ws_col);

}

これを実行すると次のようになる。

getwinsize.cの実行例

Kiloの実装が面白いのはこの先で、ioctl(2)システムコールがウィンドウサイズ(ターミナルサイズ)を取得できなかった場合に、エスケープシーケンスでサイズを計測している点にある。

kilo.cでは右に999、下に999、カーソルを移動させるエスケープシーケンスを発行している。そしてカーソルが移動した後に、前回紹介したようにカーソルの位置を調べるエスケープシーケンスを発行している。つまり、カーソルを可能な限り、端まで飛ばして、そこでカーソルの位置情報を取得すれば、すなわちそれがウィンドウサイズ(ターミナルサイズ)になっているだろうという発想だ。

この処理を本質部分だけ抜き出してさらに短くまとめると次のようになる。

エスケープシーケンスを使ってウィンドウサイズ(ターミナルサイズ)を取得するコード

#include
#include 
#include 

int
main(int argc, char *argv[])
{
    struct winsize ws;

/*
 * ioctl(2)システムコールを使ってウィンドウサイズ
 * (ターミナルサイズ)を取得し、構造体winsize wsに
 * 値を保存している。
 */
ioctl(1, TIOCGWINSZ, &ws);

printf("ws_row: %d, ws_col: %d\n", ws.ws_row, ws.ws_col);

}

下処理などを何も行っていないので途中で一旦エンターキーを押してやる必要があるが、次のようにウィンドウサイズを取得できる。

getwinsize2.cをコンパイルして実行

1度エンターキーを押す必要があるが、ウィンドウサイズ(ターミナルサイズ)を取得できる

実際には、カーソルを移動させる前に現在のカーソルの位置を取得して記憶しておき、調べ終わったあとに元の場所にカーソルを戻している。また、エディタとしてこうした処理がスムーズに行われるようにターミナルの設定も変更されている。しかし、処理の本質としては上記のようなことをやっているだけだ。

エスケープシーケンスの使用例として、なかなか面白い部分ではないかと思う。カーソルを移動させてからある処理を行って、元の位置に戻すといった処理も、エディタの実装にはなかなか役に立つ(実際このレベルでエスケープシーケンスを使ったコーディングをすることは今ではあんまりないと思うけれど)。次回はエスケープシーケンスの使用例として、シンタックスハイライトについて説明しようと思う。