ncursesは、現在の主なLinux(UNIX)環境のターミナルのCUIを操作するプログラムを作成するためのC言語関数ライブラリです。種類の異なるターミナルのエスケープコードの差異を関数で抽象化しており、ターミナルの環境によらず同じプログラムを実装できるようになっています。
ncursesは古いcursesライブラリを元に拡張したものです。cursesが作られた当時は、まだDEC社のVT100のようなキャラクタ端末装置を想定した実装であり、その後に使われるようになったGUIで動く端末エミュレータのようなものは考えられていませんでした。cursesはやがて開発が終わり、その時代の新しい環境に適応したncurses(nはnewの意)に引き継がれました。とは言ってもこれも相当古い時代の事なので全く新しいものではありません。
CUIのターミナルはUNIXからLinux、MacOS、あるいはTeratermのようなアプリまで現代でも広く使われていますが、ほとんどはDEC社のVT100のエスケープコードを基礎として拡張されたものです。その系譜ではないものにWindowsのコマンドプロンプトがあります。
ターミナルのCUIプログラムを作る方法は段階的にいくつかあり、最も直接的なのは、VT100互換のエスケープコードを直接出力することです。その次に、terminfoのケーパビリティを取得して、得られたエスケープコードのフォーマットを使って出力する方法です。そして最も一般化・抽象化されている方法がncursesライブラリの関数を使う方法です。現在使われているCUIのプログラムはncursesを使うものが多いようですが、terminfoの参照から直接エスケープコードを出力する実装方法をあえて選んでいるものもあるようです。
ncursesライブラリのインストール
ncursesでプログラムを作るには、まずlibncursesライブラリをインストールする必要があります。主要なLinuxディストリビューションではパッケージでインストールできます。
sudo apt -y install libncursesw5-dev
sudo yum install ncurses-devel
terminfoの参照によるエスケープコードの出力
ncursesの関数を使えば、エスケープコードのフォーマットを意識せずにターミナルを操作するプログラムを作成できますが、ここではまずそれより一歩原始的な、terminfoからケーパビリティのエスケープコードを引き出して利用する方法を試してみます。
ここでは以下の関数を使います。
int setupterm(const char *term, int filedes, int *errret);
|
ターミナル制御の初期化
|
char *tigetstr(const char *capname)
|
capnameに対応するエスケープのフォーマットを取得
|
char *tparm(const char *str, ...);
|
エスケープコードのパラーメータをセットする
|
int putp(const char *str);
|
標準出力へ文字列を出力する
|
int tputs(const char *str, int affcnt, int (*putc)(int));
|
putc()でエスケープコードを出力する
|
これらはncursesではなくcursesライブラリ(libcurses)の関数で、コンパイル時にはリンクオプションに-lcursesを指定します。
gcc xxx.c -lcurses -o xxx
必要なincludeヘッダはterm.hだけですが、term.hの中でcurses.hがincludeされていない場合を考慮してcurses.hも共にincludeします。
次の例は、terminfoのケーパビリティの名前(capnameと呼ぶ)からいくつかのエスケープコードのフォーマットを、tigetstr()により取得して表示しています。
#include <stdio.h>
#include <curses.h>
#include <term.h>
int main()
{
setupterm(NULL, 0, NULL);
char *capnames[] = {
"clear", // 画面クリア
"cup", // カーソル移動(行・桁指定)
"cuu", "cud", "cuf", "cub", // カーソル移動(上下右左)
"bold", // 太字
"setaf", // 文字色設定
NULL
};
for (int i = 0; capnames[i] != NULL; i++) {
printf("%s=%s\n", capnames[i], tigetstr(capnames[i]));
}
return 0;
}
cursesライブラリの関数を使う前に、setupterm()で初期化する必要があります。setupterm()は、ncursesの画面初期化を行うinitscr()の中で呼び出されています。通常は上の例のように引数に(NULL,0,NULL)を与え、すべてデフォルトとして初期化します。
ターミナルにそのまま適用されてしまうものがあるので、ファイルにリダイレクトして出力結果を確認してみます。各ケーパビリティのエスケープコードのフォーマットは次のように得られました。
$ ./capab > list.txt
clear=^[[H^[[2J
cup=^[[%i%p1%d;%p2%dH
cuu=^[[%p1%dA
cud=^[[%p1%dB
cuf=^[[%p1%dC
cub=^[[%p1%dD
bold=^[[1m
setaf=^[[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m
それぞれprintf()のフォーマットのような文字列が得られます。表示できないESC(0x1b)は「^[」のように見えて表示されます(表示のされ方は使うテキストエディタによるかもしれません)。たとえばbold=は「ESC[1m」と読み取れます。
この例の確認環境はxterm-256colorですが、この環境のターミナルでの操作は上のエスケープコードを使うことになります。tigetstr()から得られるエスケープコードのフォーマット文字列と、そのフォーマットに入れるパラメータをtperm()へ与えると、そのターミナルへ出力する文字列が完成します。次の例では、clearで画面消去してcupにより行2、桁5(始点0として)の座標にカーソルを移動します。
#include <stdio.h>
#include <curses.h>
#include <term.h>
int main()
{
setupterm(NULL, 0, NULL);
printf("%s", tigetstr("clear"));
char *cup_esc = tigetstr("cup");
printf("%s", tparm(cup_esc, 2, 5));
getchar();
return 0;
}
指定座標へのカーソル移動のcpuのエスケープコードは、最初の例のtigetstr()からこのように得られました。
cup=^[[%i%p1%d;%p2%dH
「%p1%d」「%p2%d」は「パラメータ1-整数」「パラメータ2-整数」という意味で、それぞれ行と桁の数値を当てはめます。最初の%iは、これらの指定値に+1を加算するという意味であり、左上始点を(0, 0)とした座標系で指定するための調整です。従って上の(2, 5)で指定したカーソルは、1から数えて3行目・6桁目に現れます。このようなフォーマット文字列と必要なパラメータを可変長引数でtparm()に指定すると、完成した文字列が得られます。
tparm()で完成した文字列は、そのままprintf()などで標準出力へ出力すれば、期待するカーソル移動の結果が得られます。エスケープコードには端末の遅延に関する指示が含まれるものがあり、それを考慮した出力を行うならば、出力用のcursesの関数であるputp()やtputs()を使います。putp()やtputs()にtparm()得た完成したエスケープコードを与えます。tputs()は出力用の関数を指定できます。
次の例は、画面消去(clear)して2行5桁(cup)で緑色の35番(setaf)を、putp()とtputs()で出力しています。
#include <stdio.h>
#include <curses.h>
#include <term.h>
int main()
{
setupterm(NULL, 0, NULL);
printf("%s", tigetstr("clear"));
char *cup_esc = tigetstr("cup");
putp(tparm(cup_esc, 2, 5));
char *setaf_esc = tigetstr("setaf");
tputs(tparm(setaf_esc, 35), 1, putchar); // 1=行数 putchar=出力に使う関数
printf("ABCD");
getchar();
return 0;
}
setafは文字色設定でtigetstr()からは
^[[%?%p1%{8}%<%t3%p1%d%e%p1%{16}%<%t9%p1%{8}%-%d%e38;5;%p1%d%;m
のように得られます。非常に長いですが与えるパラメータはp1の色の番号だけです。
tputs()は出力関数を指定できます。上の例ではputchar()を指定しているのでほとんどputp()と同じです。その前の数値を指定する引数(affcnt)は影響する行数の指定ですが、改行を含まないので1を指定します。
ABCD ⇒緑色です
上の例の結果はこのように、カーソル移動と色の表示をターミナルに制御した結果が得られます。
terminfoの情報は、出力時に使う文字列のエスケープコードだけではなく、そのターミナルが持っている機能、あるいは現在の状態をブール値(1=真)、もしくは数値で確認ができます。
tigetflag(capab) 真偽ケーパビリティ
tigetnum(capab) 数値ケーパビリティ
#include <stdio.h>
#include <curses.h>
#include <term.h>
int main()
{
setupterm(NULL, 0, NULL);
printf("lines=%d\n", tigetnum("lines")); // 行数(数値ケーパビリティ)
printf("cols=%d\n", tigetnum("cols")); // 桁数(数値ケーパビリティ)
printf("bce=%d\n", tigetflag("bce")); // 画面を背景色で消去するか(真偽のケーパビリティ)
return 0;
}
lines=59
cols=144
bce=1
以上のように、terminfoからcapnameに対応するエスケープコードを引き出して出力することで、種類の異なるターミナルアプリでも標準化されたcapnameにより同一の振る舞いになるようなCUIプログラムが作成できます。これを更に抽象化を進めてコーディングしやすくしたものがncursesライブラリです。
ncurses関数
ncursesの関数を使うことで、terminfoのエスケープコードを意識することなくターミナル操作を実装できます。
Hello World
次の例は、おそらく最も短いncursesの「Hello World」だと思います。
これを実行すると、ターミナルウィンドウの画面が消去し「Hello World」が表示された状態で止まります。なにかキーを押すと元のシェルの表示に戻ります。
#include <ncurses.h>
int main()
{
initscr();
printw("Hello World");
refresh();
getch();
endwin();
return 0;
}
コンパイルするときには、-lncursesのようにncursesライブラリのリンクオプションを指定します。
gcc xxx.c -lncurses -o xxx
しかし実際には我々は日本語を使うので、次のようにマルチバイト文字に対応するncurseswを指定する方がよいでしょう。この場合は同時に、機能を有効化するDNCURSES_WIDECHARの定義が必要です。#defineで定義するかもしくはコンパイラオプションで指定します。
gcc xxx.c -DNCURSES_WIDECHAR -lncursesw -o xxx
マルチバイト文字を使う場合での「Hello World」は次のようになるでしょう。
#include <ncurses.h>
#include <locale.h>
int main()
{
setlocale(LC_ALL, ""); // ロケール:""の場合は環境変数LANGが適用される
initscr();
printw("こんちは");
refresh();
getch();
endwin();
return 0;
}
上で使われているncurses関数の処理の概要は次の通りです。
initscr()
|
端末の初期化。ncursesを開始する。
|
printw()
|
カーソル位置から文字列を表示する。
printf()と同じフォーマットが使える。
|
refresh()
|
バッファをスクリーンへ反映して表示する。
|
getch()
|
キーボード入力(キー入力で即入力文字を返しEnterを待たない)
|
endwin()
|
端末制御の終了。ncursesを終了する。
|
カーソル移動・文字出力
ncursesはスクリーンの任意の行・カラムの位置に文字を表示させることができます。文字はカーソルの位置に表示されます。カーソルを表示させる座標に移動させてからprintw()などを呼び出します。
スクリーンサイズの取得
カーソルを移動するには、そのターミナルのスクリーンのサイズを知らなければなりません。VT100のような端末装置ならば、tarminfoのケーパビリティから固定的なサイズが得られますが、多くはxtermのようなGUIのウィンドウで動くターミナルエミュレータで、そのアプリのウィンドウサイズの変化でスクリーンサイズも変化します。getmaxyx()で現在のスクリーンのサイズを取得できます。
getmaxyx(WINDOW *win, int y, int x);
winにはウィンドウの構造体を指定しますが、newwin()などで作ったウィンドウでないならばデフォルトのスクリーンを示すstdscrを指定します。
ncursesの関数では座標系の順序がすべて(行, 桁)、すなわち(y, x)であることに注意してください。getmaxyx()はマクロ関数として定義されており、yとxに引数戻り値として画面サイズが返ります。
#include <ncurses.h>
int main()
{
int x, y;
initscr();
getmaxyx(stdscr, y, x);
printw("y=%d x=%d", y, x);
getch();
endwin();
return 0;
}
y=35 x=129
これとは別の方法で、LINES・COLSというグローバル変数から得ることもできます。これらはサイズの変化に追従して値が設定されます。
#include <ncurses.h>
int main()
{
int x, y;
initscr();
while (getch() != 'q') {
printw("(%3d %3d)\n", LINES, COLS);
refresh();
}
endwin();
return 0;
}
( 28 120)
( 27 111)
( 27 110)
( 26 109)
カーソル移動
printw()などの表示関数は、カーソルの位置から表示します。カーソルは画面サイズの範囲で移動できます。move()はカーソルを指定する座標へ移動します。
int move(int y, int x);
引数に与える座標は(行, カラム)の順で、(y, x)なので注意してください。行、カラムともに始点は0から数えます。画面左上(ホーム)は(0, 0)になります。
#include <ncurses.h>
int main()
{
initscr();
move(0, 0);
printw("MOVE0_0");
move(2, 2);
printw("MOVE2_2");
refresh();
getch();
endwin();
return 0;
}
MOVE0_0
MOVE2_2
printw()などで表示した文字列の末尾にカーソルが移動します。続けてprintw()でなにか文字列を表示させた場合は、「MOVE2_2」に続いて表示されます。
カーソルはデフォルトで表示されます。カーソルはcurs_set()で非表示・表示を設定できます。
curs_set(0) カーソル非表示
curs_set(1) カーソル表示
#include <ncurses.h>
int main()
{
initscr();
curs_set(0); //カーソルを非表示
printw("EraseCursor");
refresh();
getch();
endwin();
return 0;
}
表示関数
画面表示で代表的な関数は、上の例にも登場したprintw()です。printw()はC言語のprintf()と同様に、フォーマット文字列と可変長引数を指定します。
int printw(char *fmt, ...);
addstr()、addwstr()は文字列を現在のカーソル位置に追加(スクリーンバッファに追加)します。
int addstr(char *str);
int addnstr(char *str, int n);
int addwstr(wchar_t *wstr);
int addnwstr(wchar_t *wstr, int n);
#include <ncurses.h>
#include <locale.h>
int main()
{
setlocale(LC_ALL, "");
initscr();
move(0, 0);
addstr("ABCあいう");
refresh();
getch();
endwin();
return 0;
}
addwstr()はワイド文字(wchar_t型)の文字列を与えます。ワイド文字の文字列定数は「L"〜"」、文字定数は「L'〜'」のようにします。wchar_t型はUnicode(実際はUTF-16か32で環境依存があるが)を格納します。U+3041からは日本語の平仮名がはじまります。
#include <ncurses.h>
#include <locale.h>
#include <wchar.h>
int main()
{
wchar_t wc[10];
int i;
setlocale(LC_ALL, "");
initscr();
move(0, 0);
addwstr(L"ABCあいう");
refresh();
for (i = 0; i < 5 ; i++) {
wc[i] = 0x3042 + i * 2;
}
wc[i] = L'\0';
move(1, 0);
addwstr(wc);
refresh();
getch();
endwin();
return 0;
}
ABCあいう
あいうえお
端末は表示する文字の文字コードをロケール設定に従い解釈します。C言語のprintf()やncursesのprintw()の引数に与える文字列がUTF-8で書かれており、ロケール設定の文字コードがUTF-8ならば正しく表示されます。setlocale()により、wchar_t型の文字列を格納するときのソースコードの文字列を、ロケール設定の文字コードで解釈できます。
全角文字の座標は、文字としては1文字でも表示上では2桁になります。次の例の「右左」の右は、「西」の後ろ半分の座標に重なっていますが、自動的に調整されて「ABC東西右左」と表示されます。
#include <ncurses.h>
#include <locale.h>
int main()
{
setlocale(LC_ALL, "");
initscr();
mvprintw(0, 0, "ABC");
mvprintw(0, 3, "東西"); // 西と次の右が6桁目で重なっている
mvprintw(0, 6, "右左");
refresh();
getch();
endwin();
return 0;
}
ABC東西右左
クリア
clear()とerase()はどちらもスクリーンを消去します。
int erase(void);
int clear(void);
erase()は、画面全体を空白で埋めることで消去し、clear()はターミナルへの初期化を指示して消去します。どちらも消去前の属性は保持されます。
#include <ncurses.h>
int main()
{
initscr();
printw("ABCDEFG");
getch();
clear();
getch();
printw("0123456");
getch();
erase();
getch();
endwin();
return 0;
}
ncurses関数のプリフィックスについて
ncursesの関数には先頭の引数で「カーソル移動」と「ウィンドウ」を指定できる共通の引数定義になっているものがあります。それらの関数にはプリフィックスとして「mv」や「w」、「mvw」が付きます。
mv__(int y, int x, ...) カーソル移動と〜
w__(WINDOW *win, ...) ウィンドウ指定と〜
mvw__(WINDOW *win, int y, int x, ...) カーソル移動をウィンドウ指定と〜
ウィンドウを指定しないmv〜は、デフォルトのスクリーンのstdscrが暗黙に適用されます。
例えばprintw()の仲間には、カーソル移動を合わせたmvprintw()などがあります。
int printw(char *fmt, ...);
int wprintw(WINDOW *win, char *fmt, ...);
int mvprintw(int y, int x, char *fmt, ...);
int mvwprintw(WINDOW *win, int y, int x, char *fmt, ...);
#include <ncurses.h>
int main()
{
initscr();
mvprintw(2, 4, "MOVE2_4");
refresh();
getch();
endwin();
return 0;
}
printw()以外にも、addstr()ならばmvaddstr()、delch()はmvdelch()のように、スクリーン出力関数の多くがこのようなグループで構成されています。
キーボード入力
ncursesにはキー入力を行う関数にgetch()があります。getch()はEnterを待たず、キーを押した時点でそのキーの文字を返します。
通常では矢印キーなどの特殊なキーが押されたときにgetch()で取得すると、ESC→"["→"B"のようにESCから続く複数の値が連続して取得されます。keypad()で特殊キー入力を有効化すると、そのような特殊キーをcurses.hで定義されている単独の整数値で取得できます。
表示可能な文字はgetchar()などと同様にASCIIコードで得られます。
int keypad(WINDOW *win, bool bf);
getch()を呼び出しキーを押したとき、デフォルトではその文字がスクリーンに表示(エコーバック)されます。noecho()を呼び出すと、キー入力をエコーさせないように設定します。echo()はその逆で、明示的にキー入力を表示させる設定にします。
int noecho(void);
int echo(void);
次の例は、押したキーのncursesが定義するキーコードを取得し、コードの値を16進数と8進数で表示します。
#include <ncurses.h>
int main()
{
int key = 0;
initscr();
noecho(); // キー入力を表示しない
keypad(stdscr, TRUE); //特殊キー(カーソルキーなど)を有効化
while (key != 'q') {
key = getch();
printw("key=%c %04x %04o\n", key, key, key);
refresh();
}
endwin();
return 0;
}
キーコードはcurses.hでKEY_〜のマクロ定数で定義されています。主なncursesが定義する特殊キーのキーコードとマクロ定数の定義は次の通りです。
キー
|
HEX
|
OCT
|
マクロ定義
|
下
|
0x102
|
0402
|
KEY_DOWN
|
上
|
0x103
|
0403
|
KEY_UP
|
左
|
0x104
|
0404
|
KEY_LEFT
|
右
|
0x105
|
0405
|
KEY_RIGHT
|
HOME
|
0x106
|
0406
|
KEY_HOME
|
BS
|
0x107
|
0407
|
KEY_BACKSPACE
|
F1〜Fn
|
0x109+n
|
0411+n
|
KEY_F(n)
|
DEL
|
0x14A
|
0512
|
KEY_DC
|
INS
|
0x14B
|
0513
|
KEY_IC
|
PageDown
|
0x152
|
0522
|
KEY_NPAGE
|
PageUp
|
0x153
|
0523
|
KEY_PPAGE
|
END
|
0x168
|
0550
|
KEY_END
|
キー入力のバッファリング
getchar()のように、通常のキー入力はEnterの入力までバッファリングします。getch()は、通常のターミナルはバッファリングせずEnterを待たずに入力を返します。しかし、ターミナルによってはバッファリングするモードに設定されている場合があります。そのような場合でも強制的にバッファリングしない設定にするには、最初にcbreak()を呼び出しておきます。
int cbreak(void);
int nocbreak(void);
cbreak()の逆のバッファリング有効化はnocbreak()です。
ncursesのプログラムの最初の準備で、initscr()に続いてcbreak()を明示的に呼び出すと確実です。
initscr();
cbreak();
keypad(stdscr, TRUE);
noecho();
...
文字列のキー入力
getstr()は、gets()やfgets()と同様にEnterの入力までの文字列の入力を格納します。
#include <ncurses.h>
#include <locale.h>
int main()
{
char s[80];
setlocale(LC_ALL, "");
initscr();
echo();
printw(">>>");
getstr(s);
printw("[%s]\n", s);
getch();
endwin();
return 0;
}
>>>あいう
[あいう]
この例ではキー入力をエコー表示しないnoecho()ではなく、エコーバックするecho()をあえて指定しています。noecho()を指定すると「>>>」に続いてキー入力する文字が表示されなくなります。
RAWモードによるすべてのキー入力受付
keypad()により特殊キーを許可しても、CTRL-Cでプログラムが終了してしまいます。raw()によりRAWモードを指定すると、そのような制御コードのキー入力も忠実に取り込むことができるようになります。
次の例は、CTRL-Cを押してもプログラムが終了せず取得します。F1キーで終了します。
#include <ncurses.h>
int main()
{
int ch;
initscr();
raw(); // RAWモード
keypad(stdscr, TRUE);
noecho();
while (1) {
if ((ch = getch()) == KEY_F(1)) {
break;
} else {
printw("key=%c(%02x)\n", ch, ch);
}
refresh();
}
endwin();
return 0;
}
key=^A(01)
key=^B(02)
key=^C(03)
key=^D(04)
key=R(152)
key=S(153)
key=^B(102)
key=^C(103)
CTRLと組み合わせるキーはASCIIコードの0x01〜0x1fの制御コードに対応しています。
もし入力バッファリングをしないモードにするcbreak()を呼び出すならば、cbreak()をraw()の前に呼び出して下さい。cbreak()がraw()の設定を上書きしてしまうからです。
キー入力のタイムアウト
通常のgetch()は、入力があるまで関数がブロッキング(待機)します。halfdelay()により入力にタイムアウトを設定すると、getch()呼び出しから指定する時間が経過するとブロッキングを解除してエラー(-1)を返します。
int halfdelay(int tenths);
tenths: 0.1秒単位のタイムアウト
次の例は、halfdelay()でキー入力を1秒でタイムアウトしています。しばらくキーを押さないでいると、getch()が1秒周期で-1を返す様子がわかります。
#include <ncurses.h>
int main()
{
int ch;
initscr();
cbreak();
keypad(stdscr, TRUE);
halfdelay(10);
noecho();
while (1) {
if ((ch = getch()) == 'q') {
break;
} else {
printw("key=%c(%d)\n", ch, ch);
}
refresh();
}
cbreak();
getch();
endwin();
return 0;
}
key= (-1)
key= (-1)
key=a(97)
key= (-1)
この例は、「q」によりループを出て、最後のgetch()により何かキーを押すとendwin()で終了しています。最後のgetch()の前にcbreak()を呼び出しているのは、halfdelay()の設定を解除して、デフォルトのブロッキングモードに戻るためです。このcbreak()を呼び出さなければ、最後のgetch()も1秒でタイムアウトして、勝手にプログラムが閉じることになります。
ESCキーのタイムアウト
getch()は、通常はキー入力があれば即座にキーのコードを戻り値に返しますが、例外としてESCキーの場合は、ESCキーを押してから1秒経過してから戻り値が返ります。これはESC(0x1b)に続くエスケープシーケンスを待っているからです。
set_escdelay()は、ESCキーに続く入力待ちのタイムアウトを設定します。ESCキーで何かのメニューやモードを終わらせるようなものを作りたい場合は、set_escdelay()で短い時間を設定するとよいでしょう。
int set_escdelay(int ms);
ms: タイムアウトをミリ秒で指定
次の例は、ESCキータイムアウトを100msに設定しています。set_escdelay()の値をいろいろ変えるとESCを押したときの「key=〜」が表示されるレスポンスの違いがわかります。
#include <ncurses.h>
int main()
{
int ch;
initscr();
cbreak();
keypad(stdscr, TRUE);
set_escdelay(100);
noecho();
while (1) {
if ((ch = getch()) == 'q') {
break;
} else {
printw("key=%c(%02x)\n", ch, ch);
}
refresh();
}
endwin();
return 0;
}
key=^[(1b)
key=^[(1b)
key=^[(1b)
カラー・文字修飾
has_colors()またはcan_change_color()が1(真)を返せばカラー表示可能です。
あるいはterminfoのケーパビリティのcolors(最大色数)の設定でもカラー対応かどうかが確認できます。
#include <ncurses.h>
int main(int argc,char *argv[])
{
initscr();
printw("has colors=%d\n", has_colors());
printw("can change color=%d", can_change_color());
getch();
endwin();
return (0);
}
has colors=1
can change color=1
$ infocmp xterm-256color
# Reconstructed via infocmp from file: /lib/terminfo/x/xterm-256color
xterm-256color|xterm with 256 colors,
am, bce, ccc, km, mc5i, mir, msgr, npc, xenl,
colors#0x100, cols#80, it#8, lines#24, pairs#0x10000,
カラー表示を使う場合は最初にstart_color()を呼び出して初期化する必要があります。
文字色は直接カラーコードなどを指定するのではなく、事前に文字の色と文字背景の色の組み合わせの設定をinit_pair()により番号で定義します。
int init_pair(short pair, short f, short b);
pair: 定義した文字色の番号(1〜)
f: 文字色
b: 文字の背景色(文字の背景色であり画面の背景ではない)
pairは1〜の番号で、上限値はterminfoのpairsケーパビリティで設定されています。通常は十分大きな値なので上限を気にすることはないと思います。
色指定は、次のように定義されています。
COLOR_BLACK
|
0
|
COLOR_RED
|
1
|
COLOR_GREEN
|
2
|
COLOR_YELLOW
|
3
|
COLOR_BLUE
|
4
|
COLOR_MAGENTA
|
5
|
COLOR_CYAN
|
6
|
COLOR_WHITE
|
7
|
文字色の指定は文字の属性を変更することです。attrset()は文字の属性を、引数で与える属性値に設定します。文字色の属性値は、init_pair()で定義した文字色の番号(pair)をCOLOR_PAIR(pair)マクロで変換して得ることができます。
int attrset(int attrs);
COLOR_PAIR(pair) ⇒ attrsに与える文字色の属性値
#include <ncurses.h>
int main(int argc,char *argv[])
{
initscr();
start_color(); // 色の準備
init_pair(1, COLOR_RED, COLOR_YELLOW); // 色1 は黄背景に赤文字
init_pair(2, COLOR_GREEN, COLOR_WHITE); // 色2 は白背景に緑文字
init_pair(3, COLOR_YELLOW, COLOR_BLUE); // 色3 は青背景に黄文字
attrset(COLOR_PAIR(1)); // 色1 を使う 次回の変更まで設定は保持する
mvaddstr(5, 5, "Hello World");
attrset(COLOR_PAIR(2)); // 色2 を使う
mvaddstr(5, 25, "Hello World");
attrset(COLOR_PAIR(3)); // 色3 を使う
mvaddstr(5, 45, "Hello World");
getch();
endwin();
return (0);
}
attrset()を呼び出した次から表示する文字からその変更した属性が適用されます。
Linuxのターミナルエミュレータの色のスキームにより黄色が茶色に近い色に見えるなど実際の発色が変化します。その場合は、ターミナルの色のスキームの設定を変えてみてください。
デフォルト色
事前にuse_default_colors()を呼び出すと、ターミナルのデフォルトの文字色を-1として指定できるようになります。文字色に-1を指定した場合はデフォルトの文字色の適用となり、文字の背景色に-1を指定した場合はデフォルトの背景色の適用になります。また、-1に相当する文字色と背景色をassume_default_colors()で変更できます。
int use_default_colors(void);
int assume_default_colors(int fg, int bg);
#include <ncurses.h>
int main(int argc,char *argv[])
{
initscr();
start_color();
use_default_colors();
assume_default_colors(COLOR_RED, COLOR_YELLOW);
mvprintw(2, 5, "Default Color");
init_pair(1, COLOR_MAGENTA, -1); // 背景色にデフォルトを指定(この場合黄色)
init_pair(2, -1, COLOR_GREEN); // 文字色にデフォルトを指定(この場合赤)
attrset(COLOR_PAIR(1));
mvprintw(3, 5, "Default Color");
attrset(COLOR_PAIR(2));
mvprintw(4, 5, "Default Color");
getch();
endwin();
return (0);
}
COLOR_REDなどの8色は、ターミナルの設定に従った発色になります。init_color()によりそれぞれのCOLOR_〜の色のRGB値を再設定できます。
int init_color(short color, short r, short g, short b);
color: RGBを変更するCOLOR_〜
r, g, b: RGBを0〜1000の値で指定(0〜255ではないことに注意)
#include <ncurses.h>
int main(int argc, char *argv[])
{
initscr();
start_color();
init_color(COLOR_BLUE, 600, 600, 1000); // BLUEをRGB値で変更
init_pair(1, COLOR_BLACK, COLOR_BLUE);
attrset(COLOR_PAIR(1)); // 背景色が水色に近い青になる
printw("ABCDEFG");
getch();
endwin();
return 0;
}
背景色
bkgd()とbkgdset()は、ターミナルの背景色を設定します。
bkgd()は、ターミナル全体すべてのデフォルトの背景色を設定し、呼び出し後直ちに画面全体へ反映します。それに対しbkgdset()は、それを呼び出した以降から出力した文字に対して設定を反映します。
int bkgd(chtype ch);
void bkgdset(WINDOW *win, chtype ch);
bkgd()とbkgdset()は背景色を設定する関数ですが、引数にCOLOR_PAIR()で得る色の属性のペア番号をするので、実際には背景色だけでなく文字の色(フォアグラウンドの色)も同時に設定されます。
次の例は、bkgd()とbkgdset()と、attrset()による属性の変更のそれぞれの方法の、画面への反映のされ方の違いを示しています。
#include <ncurses.h>
int main(int argc,char *argv[])
{
initscr();
start_color();
use_default_colors();
init_pair(1, COLOR_YELLOW, COLOR_BLUE);
init_pair(2, COLOR_WHITE, COLOR_MAGENTA);
bkgd(COLOR_PAIR(1)); // すべての行・カラムに即座に反映
printw("Background Color\n\n");
bkgdset(COLOR_PAIR(2)); // ここからの表示から反映
mvprintw(4, 0, "Background Color"); // 4と6行目はbkgdset()が適用される
mvprintw(6, 0, "Background Color"); // 間の5行目はbkgd()の設定が残る
init_pair(3, COLOR_GREEN, -1);
attrset(COLOR_PAIR(3));
mvprintw(7, 0, "Background Color"); // デフォルト背景色の-1はターミナルの設定が適用される
getch();
endwin();
return (0);
}
文字の属性
attrset()は文字の属性を設定する関数であり、COLOR_PAIR()による色属性以外にも太字や反転などの文字修飾が指定できます。
A_BOLD
|
太字(強調)
|
A_REVERSE
|
色の反転(文字色と背景色が入れ替わる)
|
A_UNDERLINE
|
下線
|
A_BLINK
|
点滅
|
A_NORMAL
|
通常
|
これらの属性とCOLOR_PAIR()を含めて、それぞれをOR「|」で複数を重ねて設定できます。例えば、太字の下線ならば「A_BOLD | A_UNDERLINE」のようにできます。
#include <ncurses.h>
int main(int argc,char *argv[])
{
initscr();
start_color();
init_pair(1, COLOR_YELLOW, COLOR_BLUE); // 色1 青地に黄色
attrset(COLOR_PAIR(1) | A_BOLD); // 色&強調表示 「|」で属性を複数指定できる
mvaddstr(0, 0, "Hello World COLOR+BOLD");
attrset(A_BOLD | A_REVERSE); // 太字&反転表示
mvaddstr(2, 0, "Hello World BOLD+REVERSE");
attrset(COLOR_PAIR(1) | A_REVERSE); // 色の反転
mvaddstr(4, 0, "Hello World COLOR+REVERSE");
attrset(COLOR_PAIR(1) | A_BLINK | A_UNDERLINE); // 色&点滅&下線
mvaddstr(6, 0, "Hello World COLOR+BLINK+UNDERLINE");
getch();
endwin();
return (0);
}
attrset()に限らず文字の属性を指定する関数には、上のように属性の定数や色のペアをORで指定します。
例えばaddch()は1文字と、その文字の属性を同時に指定して画面表示します。
int addch(chtype ch)
ch: 文字と属性をORで設定
次の例は、ABCのそれぞれの文字に個別の属性を加えて表示しています。
#include <ncurses.h>
int main()
{
initscr();
curs_set(0);
addch('A' | A_BOLD | A_UNDERLINE);
addch('B' | A_BLINK);
addch('C');
getch();
endwin();
return 0;
}
Bだけ点滅しています。
attron()とattroff()は、現在設定されている属性から引数に与える属性をON(追加)/OFF(削除)します。
#include <ncurses.h>
int main()
{
initscr();
curs_set(0);
attron(A_BOLD | A_UNDERLINE);
mvprintw(0, 0, "Hello World BOLD+UNDERLINE ON");
attroff(A_BOLD | A_UNDERLINE);
mvprintw(2, 0, "Hello World BOLD+UNDERLINE OFF");
getch();
endwin();
return 0;
}
属性のリセット
attrset()などでA_NORMALの属性を指定することで、現在設定されている属性をリセットして通常(デフォルト)の状態に戻すことができます。
#include <ncurses.h>
int main(int argc, char *argv[])
{
initscr();
curs_set(0);
attrset(A_BOLD | A_UNDERLINE);
mvprintw(1, 0, "0123456789");
attrset(A_NORMAL); // 属性をリセットする
mvprintw(3, 0, "0123456789");
refresh();
getch();
endwin();
return 0;
}
スクロール
画面の最下段に文字列を表示し改行した場合、デフォルトではスクロールせずに最下段の行に留まって表示し続けます。最下段行の出力を改行したときに自動的にスクロールするには、最初にscrollok()でスクロールを有効に設定します。
int scrollok(WINDOW *win, bool bf);
#include <ncurses.h>
int main()
{
initscr();
scrollok(stdscr, TRUE); // スクロール可
for (int i = 0; getch() != 'q'; i++) {
printw("%03d ABCDEFG\n", i);
refresh();
}
endwin();
return 0;
}
002 ABCDEFG
003 ABCDEFG
004 ABCDEFG
005 ABCDEFG
006 ABCDEFG
007 ABCDEFG
008 ABCDEFG
009 ABCDEFG
010 ABCDEFG
011 ABCDEFG
scrollok()でスクロールを有効にした場合は、最下段に出力して改行するたびに全体が上にスクロールします。有効にしない場合は、次のように最後の行に続けて出力し続けます。
...
006 ABCDEFG
007 ABCDEFG
008 ABCDEFG
009 ABCDEFG
010 ABCDEFG011 ABCDEFG012 ABCDEFG013 ABCDEFG014 ABCDEFG
scroll()は、スクリーン全体を1行スクロールダウンさせます。scrl()は、引数に指定する行数をスクロールします。行数の値が正数ならスクロールダウン、負数ならスクロールアップします。
int scroll(WINDOW *win);
int scrl(int n);
次の例は、4行表示されている初期表示からキーを押すたびに、
(1) 1行スクロールダウン
(2) 2行スクロールアップ
(3) 4行スクロールダウン
のように表示される行がスクロールします。
#include <ncurses.h>
int main()
{
initscr();
scrollok(stdscr, TRUE); // スクロール可
mvprintw(0, 0, "001 ABCDEFG\n");
mvprintw(1, 0, "002 ABCDEFG\n");
mvprintw(2, 0, "003 ABCDEFG\n");
mvprintw(3, 0, "004 ABCDEFG\n");
refresh();
getch();
scroll(stdscr); // キーを押すと1行スクロールダウン
getch();
scrl(-2); // もう一度キーを押すと2行スクロールアップ
refresh();
getch();
scrl(4); // もう一度キーを押すと4行スクロールダウン
refresh();
getch();
endwin();
return 0;
}
(1)でスクロールアップでスクリーンから切れた001の行は、次の(2)でスクロールダウンしたときは画面上から出てくることなく消えたままスクロールします。スクロールするのは単純に今スクリーンに表示している文字を上下させるのであって、スクロールで消えた行をバッファのようなもので保持しているわけではないことがわかります。
(0) (1) (2) (3)
001 ABCDEFG 002 ABCDEFG 004 ABCDEFG
002 ABCDEFG 003 ABCDEFG
003 ABCDEFG 004 ABCDEFG 002 ABCDEFG
004 ABCDEFG 003 ABCDEFG
004 ABCDEFG
削除
delch()は、現在カーソル位置の文字を削除します。deleteln()は、現在カーソル行を削除します。
int delch(void);
int deleteln(void);
どちらも削除した所から後が自動的に前に詰められます。
#include <ncurses.h>
int main()
{
initscr();
mvprintw(0, 0, "ABCDEFG");
mvprintw(1, 0, "1234567");
mvprintw(2, 0, "HIJKLMN");
move(0, 3);
getch();
delch(); // (0, 3)の「D」を削除
refresh();
getch();
deleteln(); // 行を削除
refresh();
getch();
endwin();
return 0;
}
(1) (2)
ABCEFG 1234567
1234567 HIJKLMN
HIJKLMN
カーソル位置の取得
getyx()は現在のカーソル位置の行・桁(y, x)を取得します。
void getyx(WINDOW *win, int y, int x);
getyx()はマクロ定義で、引数yとxは引数戻り値です。
#include <ncurses.h>
int main(int argc, char *argv[])
{
int c;
int y, x;
initscr();
noecho();
while (1) {
if ((c = getch()) == 'q')
break;
addch(c);
getyx(stdscr, y, x); // 現在のカーソル座標取得
mvprintw(5, 0, "(%d %d)", y, x);
move(y, x);
refresh();
}
endwin();
return 0;
}
jjjppp
oooaaa
123 oop
(2 8)
instr()、innstr()は現在のカーソル位置から表示されている文字列を取得します。
instr()はカーソル位置からその行全体を取得します。スクリーンの文字が表示されていない部分も空白として取得します。innstr()はカーソル位置からの取得するサイズを指定します。日本語などマルチバイト文字を取得する場合は、1文字が3バイトになることがあります。どちらも戻り値には実際に読み込んだサイズが返ります。
int instr(char *str);
int innstr(char *str, int n);
#include <ncurses.h>
#include <locale.h>
int main()
{
char s[80];
int len;
setlocale(LC_ALL, "");
initscr();
mvprintw(0, 0, "ABCDEFGHIJKLMN");
mvprintw(1, 0, "01234あいうえお");
move(0, 2);
len = instr(s);
mvprintw(2, 0, "[%s] len=%d", s, len);
move(1, 4);
len = innstr(s, 4);
mvprintw(4, 0, "[%s] len=%d", s, len);
refresh();
getch();
endwin();
return 0;
}
ABCDEFGHIJKLMN
01234あいうえお
[CDEFGHIJKLMN ]
len=59
[4あ] len=4
ウィンドウ
ncursesはスクリーンの部分的な矩形領域を「ウィンドウ」として擬似的に独立したスクリーンを作ることができます。
ウィンドウの矩形は左上を(0, 0)とする独立した座標系を持ちます。ウィンドウはnewwin()により作成し、WINDOW構造体のポインタをウィンドウのハンドルとして取得します。
WINDOW *newwin(int nlines, int ncols, int begin_y, int begin_x);
nlines: 行数
ncols: 桁数
begin_y/x: 配置する座標
WINDOWポインタは、画面操作の関数名の頭に「w」「mvw」が付いた関数の第一引数に与えることで、操作対象のウィンドウを指定します。
w__(WINDOW *win, ...)
mvw__(WINDOW *win, int y, int x, ...)
WINDOWを指定しない関数は暗黙にデフォルトのstdscrの指定とみなされます。
wborder()、box()はウィンドウの罫線に使う文字を設定します。wborder()は上下縦横、更に4つの角のそれぞれの文字を指定できます。box()は縦線と横線に相当する文字だけ指定でき、0を指定するとデフォルトの罫線が適用されます。
int wborder(WINDOW *win,
chtype ls, chtype rs, chtype ts, chtype bs, chtype tl, chtype tr, chtype bl, chtype br);
ls, rs, ts, bs, tl, tr, bl, br: l=左 r=右 t=上 b=下 でそれぞれの枠の文字
int box(WINDOW *win, chtype verch, chtype horch);
verch: 縦枠の文字
horch: 横枠の文字
※枠文字に0を指定するとデフォルトの罫線が適用される
box()やwborder()を呼び出さなければ枠線は描画されません。
wrefresh()はrefresh()のウィンドウ指定版で、指定するウィンドウを更新します。ただしウィンドウを更新するときは、同時にメインのデフォルトスクリーンもrefresh()で更新する必要があります。
newwin()が返すWINDOW構造体のデータは、ウィンドウを終了するときにdelwin()で開放しなければなりません。
int delwin(WINDOW *win);
ただし、delwin()はウィンドウの表示を消去するわけではないので、表示上のウィンドウを消去するには、元のスクリーンを再描画する必要があります。
#include <ncurses.h>
int main(int argc, char *argv[])
{
WINDOW *local_win;
initscr();
noecho();
cbreak();
local_win = newwin(5, 10, 2, 3);
box(local_win, 0 , '-');
mvwprintw(local_win, 1, 1, "01234567890");
refresh(); // 何も書いてないが元スクリーンもrefreshする必要がある
wrefresh(local_win); // ウィンドウを更新
getch();
delwin(local_win);
endwin();
return 0;
}
ウィンドウは、その矩形範囲を独立した座標系を持ちます。ただし枠線の内側の座標ではなく、枠自体もウィンドウの座標に含まれます。
┌--------┐
│012345678
90 │
│ │
└--------┘
この例が示すように、ウィンドウに対し表示した文字列は、ウィンドウの中で折り返して表示します。ただしその場合は枠線については考慮されず、枠の文字の上を上書きします。
マウスイベント
ncursesはスクリーン内で発生するマウスイベントを捕捉できます。
mousemask()によりマウスイベントの種類を指定し、getmouse()によりイベント情報を取得します。マウスボタンはgetch()によりKEY_MOUSEという特殊キーの一つとして取得できます。そのため、keypad()で特殊キー入力を事前に有効にしておく必要があります。getch()ではKEY_MOUSEという特殊キーの入力が得られたら、続けてgetmouse()によりマウスイベントの詳細を取得します。
mousemask()のnewmaskには、取得したいマウスイベントを指定します。通常はALL_MOUSE_EVENTSを指定します。oldmaskには設定前のイベントが引数戻り値で返ります。マウスイベントの捕捉を止めるときに、mousemask()のnewmaskにoldmaskを指定すると元に戻ります。oldmaskが不要な場合はNULLを指定します。
mmask_t mousemask(mmask_t newmask, mmask_t *oldmask);
newmask: 取得するイベント(ALL_MOUSE_EVENTS=全て)
oldmask: 設定前のイベント(不要な場合はNULL)
getmouse()はMEVENT型の引数戻り値にイベントの情報が返ります。MEVENTのidメンバーには、複数のマウスを使う場合の識別番号です。ただし実際には識別できないことがあり、常に0の場合もあります。
xyzは座標です。がzは未使用です。(x, y)はマウスポインタが指している桁と行を示します。
int getmouse(MEVENT *event);
typedef struct {
short id; /* ID to distinguish multiple devices */
int x, y, z; /* event coordinates */
mmask_t bstate; /* button state bits */
} MEVENT;
bstateはボタンの種類とボタンのON/OFFの状態です。これはNCURSES_MOUSE_MASKマクロ関数を使えばわかりやすく確認できます。
NCURSES_MOUSE_MASK(ボタン番号, イベントの種類)
ボタン番号は、左ボタンから1〜の番号が割り当てられており、ホイールの上下は4と5に割り当てられています。
ボタン1
|
左ボタン
|
ボタン2
|
ホイール
|
ボタン3
|
右ボタン
|
ボタン4
|
ホイール上に回す
|
ボタン5
|
ホイール下に回す
|
イベントの種類は次の通りです。ボタンを押してドラッグして離すようなときは「押す」「離す」がそれぞれ個別のイベントであり、瞬間的に押して離す場合は「クリック」で検出されます。
NCURSES_BUTTON_RELEASED
|
離す
|
NCURSES_BUTTON_PRESSED
|
押す
|
NCURSES_BUTTON_CLICKED
|
クリック
|
NCURSES_DOUBLE_CLICKED
|
ダブルクリック
|
次の例は、スクリーン上でマウスボタン、ホイールの上下がごとにホタンのイベントとマウス座標を表示します。
#include <ncurses.h>
int main(int argc, char *argv[])
{
MEVENT me;
int c = 0;
mmask_t old;
initscr();
noecho();
keypad(stdscr, TRUE);
mousemask(ALL_MOUSE_EVENTS, &old);
c = getch();
while(1) {
if (c == 'q')
break;
if (c == KEY_MOUSE) {
if (getmouse(&me) == OK) {
printw("ID=%d (%3d %3d) ", me.id, me.x, me.y);
for (int i = 1; i <= 5; i++) {
if (me.bstate == NCURSES_MOUSE_MASK(i, NCURSES_BUTTON_RELEASED))
printw("BUTTON %d RELEASED\n", i);
if (me.bstate == NCURSES_MOUSE_MASK(i, NCURSES_BUTTON_PRESSED))
printw("BUTTON %d PRESSED\n", i);
if (me.bstate == NCURSES_MOUSE_MASK(i, NCURSES_BUTTON_CLICKED))
printw("BUTTON %d CLICKED\n", i);
if (me.bstate == NCURSES_MOUSE_MASK(i, NCURSES_DOUBLE_CLICKED))
printw("BUTTON %d DOUBLECLICKED\n", i);
}
refresh();
}
}
c = getch();
}
mousemask(old, NULL); // マウスイベント取得を元にもどす
getch();
endwin();
return 0;
}
ID=0 ( 39 8) BUTTON 1 CLICKED
ID=0 ( 82 11) BUTTON 5 PRESSED
ID=0 ( 19 10) BUTTON 4 PRESSED
ID=0 ( 19 10) BUTTON 4 PRESSED
ID=0 ( 44 10) BUTTON 3 CLICKED
ID=0 ( 72 10) BUTTON 1 PRESSED
ID=0 ( 72 10) BUTTON 1 RELEASED
ID=0 ( 83 10) BUTTON 1 DOUBLECLICKED
スクリーンの保存と復元
現在のターミナルのスクリーンの状態を一時的に保存し、シェルのプロンプトを起動してコマンド等を実行するなどしてからまた元のスクリーンの状態を復元することができます。
def_prog_mode()は現在のターミナルの状態を保存します。endwin()で終了した後で、reset_prog_mode()で保存した状態を復元して続きから端末制御を継続できます。
int def_prog_mode(void);
int reset_prog_mode(void);
次の例は、「Hello World」を表示し、何かキーを押すとスクリーンを保存しシェルのプロンプトを起動します。シェルをexitで終了すると、その前の「Hello World」を表示から再開します。
#include <ncurses.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
initscr();
printw("Hello World\n");
refresh();
getch();
def_prog_mode(); // ttyモードを保存
endwin(); // 一旦終了
system("/bin/sh"); // shを起動。exitで終了
reset_prog_mode(); // 保存したttyモードに復帰
refresh();
getch();
endwin();
return 0;
}
スクリーンのファイル保存と復元
スクリーンの状態をファイルに恒久的に保存し、そのファイルを元に復元することができます。
scr_dump()にファイル名を指定して呼び出すと、現在のスクリーンの表示とその属性、カーソルの位置がファイルに保存されます。scr_restore()に保存したファイルを指定して呼び出すと、そのファイルからスクリーンを復元します。
int scr_dump(const char *filename);
int scr_restore(const char *filename);
#include <ncurses.h>
int main(int argc, char *argv[])
{
int i;
initscr();
noecho();
cbreak();
for (i = 0; i < 5; i++)
mvprintw(i, 0, "%02d ABCDEFGHIJKLMN", i);
refresh();
scr_dump("./scdump.dat"); // 画面状態をファイルへ保存
getch();
clear(); // 画面クリア
getch();
scr_restore("./scdump.dat"); // 保存したファイルで画面復元
refresh();
getch();
endwin();
return 0;
}
00 ABCDEFGHIJKLMN
01 ABCDEFGHIJKLMN
02 ABCDEFGHIJKLMN
03 ABCDEFGHIJKLMN
04 ABCDEFGHIJKLMN