連携処理3(端末制御)

端末(terminal)とは, コンピュータの基本的な ユーザインタフェース (UI; user interface,キーボードから命令を受け取り,画面に文字を描く機能) を実現するためのハードウェアやソフトウェアのことである.

コマンドラインの入力に普段使っているアプリケーション, たとえば, Linux の「GNOME 端末(gnome-terminal)」や MacOSX の「ターミナル」,Windows の「コマンドプロンプト」は, 端末ソフトウェアの一種.

これまでのC言語プログラムでは,標準ライブラリの入出力関数 printf( )scanf( ) 等を利用して, 端末の入出力を実行してきた. しかし,これらの関数では,一行単位でしか入出力できないし, 白黒でしか表示できない,等の制約が多かった. 「なーんだC言語では,まったく面白味のないプログラムしか作れないのかー」 いや,それは誤解だ.

端末自身は,キーボードや画面をより自由自在に制御する機能をもっており, C言語の標準ライブラリでも,それらの機能の一部を利用できる. さらに,curses ライブラリを利用すれば, エディタやビデオゲームのような高機能な端末制御プログラムお手軽に作成できるようになる.

実は,すでに前期にも体験していました.
後日,これを利用したオリジナルゲーム開発の課題プロジェクトもあります. 今回は,そのための準備です.

参考資料:


エスケープシーケンスによる端末制御

エスケープシーケンス

端末画面上の任意の位置にカーソルを移動したり, 表示する文字の色を変更したりするには, エスケープシーケンス(escape sequence) と呼ばれる特殊な文字列を利用する.

たとえば,次のようなコマンドを実行すると, 文字の色を変更できる:

$ echo  "^[[31m Red"		# 「^[」の入力方法は下記の通り
Red
$ echo  "^[[0m Reset"
Reset
$

なお,文字列の先頭の記号 '^[' は, [Esc] キーを表わす特殊な文字である. 2 文字の文字列 "^[" ではない. この文字をコマンドラインで入力するためには, 次のように操作する:

[Ctrl]+[v] キー → [Esc] キー
端末やシェルの設定を独自にカラフルに変更してしまっている場合, 上のコマンドの効果が見えないかもしれない. 次のコマンドで色設定を無効にできるかもしれない:
$ PS1="$ "

これら Esc 文字から始まる文字列 ( "^[[31m""^[[0m") がエスケープシーケンスである. 代表的なエスケープシーケンスの例を次の表に示しておく. なお,この表では,エスケープ文字を「Esc」と表記している.

文字色の変更
Esc[30m
Esc[31m
Esc[32m
Esc[33m
Esc[34m
Esc[35mマゼンタ
Esc[36mシアン
Esc[37m
背景色の変更
Esc[40m
Esc[41m
Esc[42m
Esc[43m
Esc[44m
Esc[45mマゼンタ
Esc[46mシアン
Esc[47m
カーソルの移動
Esc[nAn 行上へ
Esc[nBn 行下へ
Esc[nCn 列右へ
Esc[nDn 列左へ
Esc[n;mHn 行目 m 列目へ
その他
Esc[0m色のリセット
Esc[2J画面のクリア
本授業では端末モデルとして最も標準的な VT100 のみを想定している. 実際には,複数種の端末モデルがあり, エスケープシーケンスは端末モデルにより異なる.
Linux,MacOSX,Windows 用のほぼ全ての端末ソフトでは, VT100 のエスケープシーケンスがサポートされている...ハズなんだが,完全ではない. ソフトによっては利用できない機能もある.

プログラミング

これらのエスケープシーケンスは,端末自身がもっている機能であるため, プログラミングの際には,何ら特別な工夫を必要とせずに利用できる. 単純に,printf( ) 関数などによって, エスケープシーケンスを端末側へ出力すれば済む.

ここでは,C言語の printf( ) 関数ではなく, 同等な機能をもつ unix コマンドの printf で, お手軽に試してみよう:

$ printf  "クリアして \e[2J"
$ printf  "\e[3;0H 3行目で \e[31m 赤にしたり\n"
$ printf  "\e[1;10H 1行目へ戻ってみたり\n"
$ printf  "\e[5;0H\e[0m 5行目で元通りに\n"
...
これまでと何がちがう? 以前作成のプログラムでは, コマンド行の下の方向にしか実行結果を表示できなかった. 一方,このプログラム(コマンド)では,行に囚われず, 画面上の好きな位置に出力できている.

ここで '\e'Esc を表わす特殊文字である. 改行記号 '\n' などと同様,2文字で1文字を表わす.

なお,'\e' は,printf にとっては特殊文字(1文字)だが, echo にとっては単なる文字列(2文字)ですよ.
実際には '^[' でも OK. (ただし,'^[' を入力できない, とほほなエディタもあるようだ.)

curses ライブラリによる端末制御

curses

エスケープシーケンスの文字列は, 人間が読んでもほとんど意味不明な「呪文(curse)」のようなものであり, それを直接利用してプログラムを作成するなんてことは非常に面倒だ. そこで,この面倒な処理を簡単化するための関数群が curses ライブラリとして用意されている.

curses の名前の由来には諸説ありそうです. 「curse(呪文)」の複数形の他, 「cursor escape-sequence」の略語とも云えそう. Wikipedia 曰く,「cursor optimization(カーソル移動処理の最適化)」の語呂合わせとのこと.
現代では,このライブラリは ncurses(new curses) と呼ばれるバージョンに置き換えられている. 元々の curses は unix の創成期(1970年代)から利用されている.

このライブラリの関数を利用すると, 端末画面上の任意の位置にカーソルを移動したり, 表示する文字の色を変更したりするプログラム (いわゆるフルスクリーンアプリケーション) が簡単に実現可能になる.

全画面表示は,現代では当たり前のことになってはいるが, コンピュータの歴史的に画期的な技術だった.

単純な curses プログラム hello.c を作ってみよう:

#include <ncurses.h>

int main(void)
{
	initscr();	// 端末制御の開始

	start_color();	// カラーの設定
	init_pair(1, COLOR_RED, COLOR_BLUE);	// 色番号1を赤文字/青地とする
	bkgd(COLOR_PAIR(1));			// 色1をデフォルト色とする

	erase();	// 画面表示
	move(10, 20);
	addstr("Hello World");
	refresh();

	timeout(-1);
	getch();	// キー入力

	endwin();	// 端末制御の終了
	return (0);
}

これをコンパイルするには,-lncurses オプションを指定し, ライブラリをリンクする必要がある:

$ cc  hello.c  -lncurses  -o hello

実行すると,メッセージが表示され,何かキーを押すと終了する.

ソースを読んでみよう. これまでのプログラムとは全然違うものに見えるだろう. とりあえず今は,細かいことについては,気にしないでよい.

まず,おなじみのヘッダファイル stdio.h とか 標準ライブラリの入出力関数 printf( )scanf( ) とかは, まったく使われていない. その代わり,ncurses.h をインクルードし, curses ライブラリの関数だけを使っている.

画面表示やキー入力のために, 標準ライブラリの標準入出力関数printf() 等)と curses ライブラリの端末入出力関数addstr( ) 等)とを 混ぜて使わないこと. 一緒に使ってもエラーにはならないが, 多分,期待した結果にはならない. 注意せよ.

ま,curses の入出力関数があれば, 標準入出力関数を使う必要はないので, 心配する必要もない. 端末入出力には,curses の関数だけを使うこと. ただし,ファイルの入出力の場合には, 端末制御とは無関係なので, 標準ライブラリの入出力関数(fprintf( ) 等)のお世話にもなる.

よく利用するヘッダファイルについては, C言語のテンプレートファイル tmpl.c 内に追記しておくと良いでしょう.

curses の関数についての詳しくは, 参考資料を参照せよ.

次のセクションから, curses ライブラリの使い方を段階的に学習して行く. ある程度まで理解できたら,自由に改造してみよう.

沢山のサンプルソースコードがあり,面倒臭そうだが, 実際にソースを編集し,実行すること. そして,「何をやったら,どうなったのか?」確認すること. さらに,「どんなことが,どうすればできそうか?」推測しよう.

Step 1. 画面への出力

基本プログラムとして,端末画面に文字列を表示してみよう. 何らかのキーを押すと終了する.

hello-1.c

#include <ncurses.h>

int main(void)
{
	initscr();

	move(10, 20);
	addstr("Hello World");
	getch();

	endwin();
	return (0);
}

これは,端末画面の上から 10 行目,左から 20 桁目の位置に 文字列 "Hello World" を表示するだけのものであり, 何かキーを押すと終了する.

行と列の位置については,0 から数える. つまり,端末的な 10 行目は,世間一般的には 11 行目に相当.

ここで利用されている関数の意味は次の通り:

なお,画面表示の関数としては, addstr() の他に,次のようなものもある:


Step 2. 画面サイズの取得

文字列表示をセンタリング(centering;中央揃え)してみよう.

hello-2.c

#include <ncurses.h>
#include <string.h>

int main(void)
{
	int	x, y, w, h;
	char	*str = "Hello World";

	initscr();
	getmaxyx(stdscr, h, w);
	y = h/2;
	x = (w - strlen(str))/2;

	move(y, x);
	addstr(str);
	getch();

	endwin();
	return (0);
}

まず,一度,実行してみよう. そして,端末ウィンドウのサイズを変更してから, 再度,実行してみよう. サイズに応じて,表示位置が自動的に調整されるハズだ.

ただし,この単純すぎるプログラムでは, 実行中にサイズを変えても追従しない. 実行前にサイズを変えること.

次の関数が登場した:

ここで,stdscr は端末画面を表わす定数である. 標準出力 stdout の curses 版だと考えればよい.

ところで,Cの基本がわかっている人は疑問に思うハズだ. なぜ,利用方法が getmaxyx(stdscr, &h, &w) ではなく, getmaxyx(stdscr, h, w) なのか?

関数プロトタイプは getmaxyx(stdscr, ポインタ1, ポインタ2) とすべきではないのか? との思いが胸を去来することもあります.
ポインタがわからないドシロートさんでもわかるような親切設計? しかし,Cについて生半可な知識があると,かえって混乱してしまいそうだ. 注意せよ.

それは,getmaxyx( ) が実は,関数ではなくマクロであるからだ. 次のような感じに引数付きマクロとして定義されている:

#define getmaxyx(画面, y, x) { y=画面の行数; x=画面の桁数; }
このマクロ定義は良い設計なのだろうか?良い子はマネしないこと!
なお,引数付きマクロについては後日説明予定.

Step 3. 画面の書き換え

次に,何かキーを押すたびに表示位置を変化するようにしてみよう. [Q]キーを押すと終了する.

hello-3.c

#include <ncurses.h>
#include <string.h>

int main(void)
{
	int	x, y, w, h;
	char	*str="Hello World";
	int	key;

	initscr();
//	noecho();		// echo();
//	cbreak();		// nocbreak();
	getmaxyx(stdscr, h, w);
	y = h/2;
	x = (w - strlen(str))/2;

	while (1) {
//		erase();
		move(y, x);
		addstr(str);
//		refresh();

		key = getch();
		if (key == 'q') break;

		y++; if (y >= h) y = 0;
	}
	endwin();
	return (0);
}

無効化(コメント化)されている部分(// 以降)を 有効化しないと(// を外さないと)うまく行かないだろう. まず,コメントのまま実行し,その後,有効化して実行し, 動作を比較してみよう.

次の関数が出てきた:

いくつかの関数は,省略しても変わらない.

ここで,refresh() の意味がよくわからないかもしれない. curses では, 実際の表示用の画面(物理画面,実画面)の他に, 内部的な作業用の画面(論理画面,裏画面)を利用している. 通常,出力の処理は,論理画面にバッファリングされる. つまり,出力しただけでは,実際の画面上に即座に表示されるとは限らない.

標準出力などと同様.

確実に表示したい場合には, 出力処理の後で refresh() が必要となっている. これで,論理画面の内容が物理画面へ確実にコピーされ, 実際に見えるようになるハズだ.

つまり,refresh() しないと, 作者的には表示したつもりの内容が 実際には表示されない可能性がある. ありがちなミスなので注意しよう.

Step 4. アニメーション

今度は,キー入力しなくても自動的に動くようにしてみよう. hello-3.c のコピーを作り,次のように変更しよう. (また後で使うので,hello-3.c は書き換えずに残しておこう.)

hello-4.c

...
#include <unistd.h>		// usleep()

int main(void)
{
	...
	initscr();
	noecho();
	cbreak();
	timeout(0);
	...

	while (1) {
		...
		usleep(100000);
	}
	endwin();
	return (0);
}

次の関数を追加しただけ:


Step 5. キーボードによる操作

前のコードでは,下方向にしか移動できなかった. 今度は,カーソルキーの入力によって上下左右に移動できるようにする. hello-3 を元にして,次のように書き換えよう.

hello-5.c

...

int main(void)
{
	...
	initscr();
	noecho();
	cbreak();
	keypad(stdscr, TRUE);
	...
	while (1) {
		...
		key = getch();
		if (key == 'q') break;
		switch (key) {
		case KEY_UP:	y--; break;
		case KEY_DOWN:	y++; break;
		case KEY_LEFT:	x--; break;
		case KEY_RIGHT:	x++; break;
		}
	}
	endwin();
	return (0);
}

新たな関数はひとつだけ:

さてここで, getch( ) の戻り値の型が char ではなく int だったことの理由を明かそう. ASCII コード(1バイトの数値,0x00 〜 0xFF) だけでは表わしきれない程に多数ある特殊キーにも 番号(4バイトの数値,0x00000100 〜 0xFFFFFFFF)が割り当てられている. また,各特殊キーには, わかり易いマクロ名も定義されている:


Step 6. カラーの利用

今度は,カラーを使ってみる. ソースファイルをゼロから作成しよう.

color.c

マウスでコピペでもOKですが, 同じようなコードの羅列なので, エディタの効率的な操作の練習台として, キーボードで手打ちしてみては?
#include <ncurses.h>

int main(void)
{
	// 端末の準備
	initscr();

	// 色の準備
	start_color();
	init_pair(1, COLOR_RED, COLOR_BLUE);	// 色1 は青地に赤文字
	init_pair(2, COLOR_GREEN, COLOR_BLUE);	// 色2 は青地に緑文字
	init_pair(3, COLOR_YELLOW, COLOR_BLUE);	// 色3 は青地に黄文字
	init_pair(10, COLOR_WHITE, COLOR_BLUE);	// 色10 は青地に白文字
	bkgd(COLOR_PAIR(10));			// 背景は色10

	// 表示
	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");

	attrset(COLOR_PAIR(1) | A_BOLD);		// 色&強調表示
	mvaddstr(6,  5, "Hello World");
	attrset(COLOR_PAIR(2) | A_BOLD);
	mvaddstr(6, 25, "Hello World");
	attrset(COLOR_PAIR(3) | A_BOLD);
	mvaddstr(6, 45, "Hello World");

	attrset(COLOR_PAIR(1) | A_REVERSE);		// 色&反転表示
	mvaddstr(7,  5, "Hello World");
	attrset(COLOR_PAIR(2) | A_REVERSE);
	mvaddstr(7, 25, "Hello World");
	attrset(COLOR_PAIR(3) | A_REVERSE);
	mvaddstr(7, 45, "Hello World");

	attrset(COLOR_PAIR(1) | A_REVERSE | A_BOLD);	// 色&反転&強調
	mvaddstr(8,  5, "Hello World");
	attrset(COLOR_PAIR(2) | A_REVERSE | A_BOLD);
	mvaddstr(8, 25, "Hello World");
	attrset(COLOR_PAIR(3) | A_REVERSE | A_BOLD);
	mvaddstr(8, 45, "Hello World");

	// 終了
	getch();
	endwin();
	return (0);
}

端末の種類や設定によっては,うまく働かない機能もあるかもしれないが, あまり気にしないでおこう.

ここで,attrset() の引数の数式にある記号「|」は, 論理和(OR)演算子であり, 複数の属性値(ビット列)の任意の組み合わせを1個の引数にまとめるためのものだ. 例えば,複数の属性A(ビット列0001=値1),B(0010=2),C(0100=4), ...の内,2個の属性AとCとを指定したい場合, これらの論理和A|C(ビット列0101=属性値5)の 1個の引数だけを与えれば済む.
なお,算術和(通常の加算,+)じゃダメなんですか? 誤って同じ属性を複数回指定してしまうと, 例えば,A+A=Bとか,まったく別の属性になってしまう. しかし,論理和ならA|A=Aなのでセーフ.
そんな間違いなんて,しないって? 属性の数式を変数に代入して利用する場合, その属性を指定済みだったかどうか, ソースコード全体を調べないと間違いを発見できませんよ. 論理和なら,何度でも指定して構わないので, 昔どこかであったことは気にせず, 今そこですべきことだけに集中できます. ちなみに,どの属性が指定されているか, 論理積(AND)演算子「&」で調べることも可能.

補足:日本語の利用

全角日本語文字列(というかマルチバイト文字列)も利用できる:

#include <ncurses.h>
#include <locale.h>	// setlocale() に必要

int main(...)
{
	setlocale(LC_ALL, "");		// システム側の言語環境(日本語)を利用するよ
	initscr();
	...
	addstr("こんにちWorld");	// 全角・半角の混合文字列も使えるよ
	...
}

コンパイル方法: (ncurses ではなく,ncursesw ライブラリを使用)

$ cc  ...  -lncursesw  ...
システムによっては,-ltinfo のリンクも必要かもしれない.

このように,文字列全体を表示するだけなら,とても簡単だ. しかし,全角文字では,2バイト以上で1文字を表現していたり, 画面上で半角2文字分の幅を使ったりするので, 下の練習問題2のように, 1文字ずつとか1バイトずつに分解して処理することは難しい.

無理ではない.しかし,文字コードに関する高度な知識が必要. したがって,とりあえず, 文字列分解処理が必要な場合には,全角文字列を使わない ってことで妥協しよう. たとえば,今回の練習問題 Q2.a では, 文字列を日本語に変えると動作保証対象外としてよい.

上級者向け情報: ワイド文字(日本語文字というか英語以外の文字)用のデータ型 wchar_t, マルチバイト列(Cの標準の文字列)とワイド文字列の変換関数 mbtowc(), etc. で検索してみよう.


練習問題

Q1. アニメーションのスタート/ストップ機能を実装せよ.

プログラム hello-4 では, プログラムの開始と同時にアニメーションも開始してしまう. アニメーションのスタート/ストップについて, キーボード操作によって自由にコントロールできるように改良せよ.

例えば,プログラム実行直後には文字列の移動を止めておき, [Space]キーを押すたびに,文字列の移動の開始/停止を繰り返せるようにしよう.

更には,文字列が画面からハミ出した場合, 文字列を元の位置に戻し,移動を止めてみよう. はい,シューティングゲームのミサイルの完成です.
上級者は,更に更に,重力加速度を採り入れて,移動速度を変化させてみよう. はい,ジャンプです. この場合,座標・速度・加速度は,整数ではなく,実数とすべきでしょう.

ヒント:移動中フラグとか移動速度とかの変数を追加すればできるだろう.


Q2. ハミ出しによる画面の乱れを抑制せよ.

プログラム hello-5 で左右に移動しているとき, 文字列の一部が画面からハミ出すと表示が乱れてしまう. この不具合への対策を施せ

  1. 画面内からハミ出さないように, 文字列の移動範囲を制限するように改良せよ.

    ヒント:画面右側の範囲チェックでは, 文字列の末尾の座標を調べる必要があるだろう.

  2. (上級者用問題)ハミ出した部分文字列(文字列全体ではなく)が 画面の反対側から(右にハミ出ると左から,左にハミ出ると右から) 出てくるように改良せよ.

    ヒント:addstr( ) ではなく, addch( ) または mvaddch( ) を利用するとよい. つまり,文字列を一度に出力するのではなく, 一文字ずつ位置を指定しながら出力するとよい. このとき,当然,各文字の出力位置が画面内に収まるようにすること.

範囲のチェックの際,座標を 0 から数えることに注意. たとえば画面の「幅」が w の場合, 画面内の「座標」の範囲は 0wー1 となる. 0 w ではない!!

全画面アプリの開発例:時計

Step 0. 標準ライブラリの時間関数の利用

PC に内蔵されているタイマを利用し, 時計アプリを作ってみよう.

時計機能の練習として,まずは curses を使わず,時刻を標準出力してみる.

要するに,unix コマンドの date の劣化版クローンを作成.

clock-0.c

#include <stdio.h>	// まずは,curses ではなく,標準入出力ね
#include <time.h>	// time(), localtime(), strftime()

/*
現在の時刻文字列を生成する関数
buf:時刻文字列バッファ(参照渡し)
n:文字列バッファのサイズ
*/
void GetTimeStr(char *buf, int n)
{
	time_t		t;	// 現在の unix 時刻が入るよ
	struct tm	*tm;	// 時刻要素構造体へのポインタが入るよ

	t = time(NULL);		// 現在の unix 時刻を取得
	tm = localtime(&t);	// unix 時刻を時刻要素(年月日時分秒)へ分解
	strftime(buf, n, "%H:%M:%S", tm);	// 時刻文字列(時:分:秒)を生成
}

#define BUFLEN	10	// 文字列バッファの配列サイズ

int main(void)
{
	char	buf[BUFLEN];	// 時刻文字列が入るよ

	GetTimeStr(buf, BUFLEN);	// 時刻文字列を取得
	printf("%s\n", buf);		// 時刻文字列を表示

	return (0);
}
まだ,全画面プログラムではないので,コンパイル方法に注意. (-lncurses は不要.) 次のステップでフルスクリーン化します.

新出の関数:(#include <time.h>


Step 1. cursesライブラリとの連携

clock-0.c を元にして,フルスクリーン化してみる.

clock-1.c

// #include <stdio.h>
#include <ncurses.h>
#include <string.h>
#include <time.h>

/*
デジタル時計を表示する関数
s:時刻文字列
*/
void DrawClock(char *s)
{
	int	x, y;		// 表示位置
	int	w, h;		// 画面サイズ

	getmaxyx(stdscr, h, w);	// 画面サイズを取得
	x = (w - strlen(s))/2;	// 中央表示のための位置を算出
	y = h/2;

	mvaddstr(y, x, s);	// 時刻を表示
}

...
void GetTimeStr(char *buf, int n)
{
	...
}

...

int main(void)
{
	char	buf[BUFLEN];
	int	key;		// 入力キー文字が入るよ

	// 端末の初期化
	initscr();
	curs_set(0);		// カーソルは表示しないよ
	noecho();
	cbreak();
	timeout(0);		// キー入力は待たないよ

	// 時計の表示
	while (1) {
		GetTimeStr(buf, BUFLEN);

		erase();
		DrawClock(buf);			// 時刻文字列を表示
		refresh();

		key = getch();
		if (key == 'q') break;		// [Q]キーで終了
	}

	// 終了
	endwin();

	return (0);
}
Step 2. 表示方法の改善

表示方法をお洒落 (レトロな 7セグメント表示器)にしてみる.

clock-2.c

#include <ncurses.h>
#include <ctype.h>
#include <string.h>
#include <time.h>

#define SEG_W	4	// 7セグ1文字分の幅
#define SEG_H	5	// 7セグ1文字分の高さ

/*
コロンを描く関数
y, x:表示位置
*/
void DrawColon(int y, int x)
{
	mvaddch(y+1, x+1, '#');
	mvaddch(y+3, x+1, '#');
}

/*
7セグメントの文字を表示する関数
y, x:表示位置
seg:点灯パターン文字列(7文字+終端記号)
*/
void Draw7SegChar(int y, int x, char *seg)
{
	if (seg[0] == '1') mvaddstr(y+0, x+0, "###");

	if (seg[1] == '1') {
		mvaddstr(y+0, x+0, "#");
		mvaddstr(y+1, x+0, "#");
		mvaddstr(y+2, x+0, "#");
	}

	if (seg[2] == '1') {
		mvaddstr(y+0, x+2, "#");
		mvaddstr(y+1, x+2, "#");
		mvaddstr(y+2, x+2, "#");
	}

	if (seg[3] == '1') mvaddstr(y+2, x+0, "###");

	if (seg[4] == '1') {
		mvaddstr(y+2, x+0, "#");
		mvaddstr(y+3, x+0, "#");
		mvaddstr(y+4, x+0, "#");
	}

	if (seg[5] == '1') {
		mvaddstr(y+2, x+2, "#");
		mvaddstr(y+3, x+2, "#");
		mvaddstr(y+4, x+2, "#");
	}

	if (seg[6] == '1') mvaddstr(y+4, x+0, "###");
}

/*
セグメントの配置:(この図から忖度(そんたく)してね↑ ↓ )
	0 ━━
	1┃  ┃2
	3 ━━
	4┃  ┃5
	6 ━━
*/

/*
7セグメントのデジタル時計を描く関数
s:時刻文字列
*/
void Draw7SegClock(char *s)
{
	int	x, y;		// 表示位置
	int	w, h;		// 画面サイズ

	static char	*seg[] = {	// 7セグの点灯パターン文字列
		"1110111",	// 文字0(中央のセグ3だけ消灯)
		"0010010",	// 文字1(右端のセグ2と5だけ点灯)
		"1011101",	// 文字2
		"1011011",	// 文字3
		"0111010",	// 文字4
		"1101011",	// 文字5
		"1101111",	// 文字6
		"1010010",	// 文字7
		"1111111",	// 文字8
		"1111011"	// 文字9
	};

	getmaxyx(stdscr, h, w);		// 画面サイズを取得
	x = (w - strlen(s)*SEG_W)/2;	// 中央表示のための位置を算出
	y = (h - SEG_H)/2;

	while (*s != '\0') {
		if (isdigit(*s)) {
			Draw7SegChar(y, x, seg[*s - '0']);	// 数字を描く
		} else {
			DrawColon(y, x);			// コロンを描く
		}
		x += SEG_W;	// 表示位置を次の桁へ
		s++;		// フラグ文字列を次の桁へ
	}
}

...
void GetTimeStr(char *buf, int n)
{
	...
}

...

int main(void)
{
	...
	while (1) {
		...
		erase();
		Draw7SegClock(buf);		// 時刻を7セグ表示
		refresh();
		...
	}
	...
}
なぜ,static変数にしたのか? 通常の変数(auto変数)だと,同じ内容の初期化が何度も (この関数を呼び出す度に)繰り返され,非効率. static なら,初期化が1度だけで済み,効率的.

さらに,カラーを使えば, もっと,お洒落にできるだろう.