VT100互換ターミナル
Linuxのコマンドを打つターミナル(端末)アプリのほとんどは、「VT100互換ターミナル(端末)」と呼ばれており、初期の時代のコンピュータのキャラクタ端末として使われていたDEC(Digital Equipment Corporation)社のVT100の制御方式を引き継いでいます。
VT100(Wikipedia)
VT100はキーボードとディスプレイから構成される装置で、パソコンのような姿をしていますがOSはなく、プログラムを実行する能力はありません。ホストコンピュータと通信線(RS-232Cなど)で接続し、ホストとの通信を通して文字表示とキーボード入力だけを処理する原始のコンピュータシステムにおけるユーザインタフェースでした。
現代のUNIXの系譜を引き継ぐ環境で使われるターミナルアプリ(Linuxデスクトップアプリのターミナル、macOSのターミナル、ssh/telnetでよく使われるTeraTerm、WSLで使われるWindows Terminal、等)は、かつてのRS-232Cで行っていた制御コードのやりとりを、OS内の仮想的なしくみやIPネットワークに置き換えてCUI機能を提供しています。
VT100の制御コードのことを、ESC(0x1b)からはじまる制御方式なので主にエスケープコード、エスケープシーケンスと呼び、ANSIで標準化されているのでANSIエスケープコードと呼ぶことがあります。
LinuxのGUIアプリの「ターミナル」「端末」はVT100互換をエミュレーションする「ターミナルエミュレータ」です。ターミナルエミュレータが起動するとき、CUIで対話的に入出力するシェルプロセスが生成されます。シェルプロセスには/dev/pts/Nの疑似端末のデバイスファイルが用意され、これを通してキーボード入力や文字の表示を行うことになります。シェルから実行するC言語プログラムがprintf()を呼び出すとき、そのシェルが接続するデバイスファイルである/dev/pts/Nを通して、ターミナルエミュレータに文字列が渡り表示されます。またキーボード入力はその逆方向にC言語のプログラムに送信されgetchar()などで取得することができます。
C言語でprintf()等で文字列を表示するプログラムを実行すると、ターミナルのデフォルトの文字色で表示されますが、VT100のエスケープコードを使うと、カラー文字や太字などが表示できます。
VT100エスケープコードは、ESC(0x1b)に続くフォーマットで、
ESC[パラメータ
例)
ESC [ 15;25H 15行/25桁へカーソル移動
のようになります。パラメータの指定により、表示色を変更したり画面を消去したりスクロールしたりなど制御します。
もちろん、ターミナルの標準出力へエスケープコードを出力できるならばプログラムはC言語に限りません。
UNIXには古くからncursesという、VT100互換エスケープコードを含む複数種の端末制御のCライブラリがあり、複雑な書式のエスケープシーケンスを、直感的なAPI関数呼び出しで扱うことができます。
本稿ではncursesを使わずにLinuxのターミナルエミュレータ上で、直接のエスケープコードでCUIを制御する例を紹介します。
端末タイプと端末のCapability
xtermなどのLinuxターミナルエミュレータは、そのアプリが使用可能な端末機能のエスケープコード一覧を、terminfoと呼ばれるデータベースに格納しています。vimのようなCUIのスクリーンを使ったプログラムは、そのterminfoを参照していろいろな環境でのターミナル上で動作できるように実装されています。
terminfoを参照するには、まず現在の端末タイプを知る必要があります。端末タイプは環境変数”TERM”に設定されています。Ubuntuでは、xterm-256colorという設定でした。
$ printenv TERM
xterm-256color
terminfoデータベースの格納場所は、多くの場合は次のどれかだと思います。
/usr/share/terminfo
/usr/lib/terminfo
/lib/terminfo
Ubuntu22では/usr/lib/terminfo(/usr/libと/libはリンク)に格納されているようです。そしてターミナルタイプがxterm-256colorならば、
/usr/lib/terminfo/x/xterm-256color
のバイナリファイルに、そのターミナルの機能が記述されています。infocmpコマンドで、そのバイナリファイルを見てわかるテキストに変換して表示できます。
$ 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,
acsc=``aaffggiijjkkllmmnnooppqqrrssttuuvvwwxxyyzz{{||}}~~,
bel=^G, blink=\E[5m, bold=\E[1m, cbt=\E[Z, civis=\E[?25l,
clear=\E[H\E[2J, cnorm=\E[?12l\E[?25h, cr=\r,
csr=\E[%i%p1%d;%p2%dr, cub=\E[%p1%dD, cub1=^H,
cud=\E[%p1%dB, cud1=\n, cuf=\E[%p1%dC, cuf1=\E[C,
cup=\E[%i%p1%d;%p2%dH, cuu=\E[%p1%dA, cuu1=\E[A,
cvvis=\E[?12;25h, dch=\E[%p1%dP, dch1=\E[P, dim=\E[2m,
〜中略〜
smglr=\E[?69h\E[%i%p1%d;%p2%ds,
smgrp=\E[?69h\E[%i;%p1%ds, smir=\E[4h, smkx=\E[?1h\E=,
smm=\E[?1034h, smso=\E[7m, smul=\E[4m, tbc=\E[3g,
u6=\E[%i%d;%dR, u7=\E[6n, u8=\E[?%[;0123456789]c,
u9=\E[c, vpa=\E[%i%p1%dd,
terminfoの読み方
terminfoについてはLinux manualページに正確に記載されています。
terminfo(5) manページ - LeMoDa.net
terminfo(5) — Linux manual page
上の表示のフォーマットを簡単に紹介すると以下のようなルールがあります。
フィールドは「,」で区切る。「,」自身は0x72かバックスラッシュでエスケープする。
最初の行は、ターミナルの省略名と長い名前を「|」で区切って示している。
次の行からは、Capabilityのリストである。
Capabilityは、機能を表す略名がある。
略名に「#」か「=」が続くものは設定値があり、略名のみの場合はその機能が有効であることを表す。
存在しない機能の略名は表示されない。
「#」に続く設定値は整数値を表す。
「=」に続く設定値は文字列
「\E[」はエスケープコードの開始「ESC[」を表し、それに続く「%〜」はパラメータを意味します。例えば、
cud=\E[%p1%dB
の「%p1%d」の部分は「パラメータ1・整数」を意味します。1つのエスケープコードに複数のパラメータがある場合は%p2%dのようになります。
VT100互換エスケープコードを使った例
スクリーンクリア
ターミナル画面全体を消去します。シェルのプロンプトも消えます。カーソルは左上に移動します。
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("\e[H\e[2J"); // スクリーンクリア
getchar();
return 0;
}
カーソル移動(ホーム・絶対座標)
カーソルを指定する座標に移動します。
\e[r;cH 指定座標移動
r=行(最上段行を1とする)
c=カラム(左端を1とする)
\e[H ホーム(最上段左端)に移動
エスケープコードは行を先、桁を後に指定するので、一般的な(x,y)座標とは逆なので注意してください。また、最小値が0ではなく1から始まることにも注意が必要です。座標指定を省略した場合はホーム(最上段左端)に移動します。
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("\e[H\e[2J"); // スクリーンクリア
printf("\e[2;5H"); // カーソルをrow=2 col=5へ移動
getchar();
printf("\e[H"); // ホームに移動 ホームというのはr1-c1
getchar();
return 0;
}
カーソル移動(相対座標)
現在のカーソル位置から相対的な位置へ移動します。
\e[nA 上にn行移動
\e[nB 下にn行移動
\enC 右にn桁移動
\e[nD 左にn桁移動
この例は、カーソルがホームから半時計回りに四角を描くように回ります。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("\e[H\e[2J"); // スクリーンクリア
printf("\e[H"); // ホームに移動
fflush(stdout);
sleep(1);
printf("\e[2B"); // 下2移動
fflush(stdout);
sleep(1);
printf("\e[3C"); // 右3移動
fflush(stdout);
sleep(1);
printf("\e[2A"); // 上2移動
fflush(stdout);
sleep(1);
printf("\e[3D"); // 左3移動
fflush(stdout);
getchar();
return 0;
}
printf()だけを呼び出しただけではすぐにはスクリーン上には反映されないので、標準出力をfflush()により強制的に反映させています。
全角文字のカーソル移動
2桁分の座標に配置する全角文字の上にカーソルを表示するとき、2桁を左半分、右半分のように表示するのではなく、その2桁のどちらの座標でも1文字の前の方にカーソルがとどまります。
次の例では、Enterを押すたびに「ABCあいう東西南北」を左から右へカーソルが移動します。全角文字の上ではEnter2回分が同じ文字にとどまります。
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
printf("\e[H\e[2J"); // スクリーンクリア
printf("ABCあいう東西南北");
for(i = 1; i < 18; i++) {
printf("\e[1;%dH", i);
getchar();
}
return 0;
}
カーソル非表示・再表示
カーソルを非表示、再表示させます。
\e[?25l カーソル非表示
\e[?12l\e[?25h カーソル表示
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("\e[H\e[2J"); // スクリーンクリア
printf("\e[H"); // ホームに移動
getchar();
printf("\e[?25l"); // カーソル非表示
getchar();
printf("\e[?12l\E[?25h"); // カーソル表示
// getchar()でEnter2回なので3行目に再出現する。
getchar();
return 0;
}
文字色
文字色は、ターミナルのケーパビリティにより8色・16色・256色で表示できます。ターミナルエミュレータが何色が可能かはterminfoの設定(infocmpで表示できる)で確認できます。ここの例で使用したターミナルでは256色表示が可能です。
# Reconstructed via infocmp from file: /lib/terminfo/x/xterm-256color
xterm-256color|xterm with 256 colors,
エスケープコードでの文字色の指定は、8・16・256を段階的に異なるコードで指定します。
\e[3cm c=0〜7 8色まで
\e[9cm c=8〜15 16色まで
\e[38;5;cm c=16〜255 256色
また、反転文字の色(背景色)を指定できます。
\e[4cm c=0〜7 8色まで(反転)
\e[10cm c=8〜15 16色まで(反転)
\e[48;5;cm c=16〜255 256色(反転)
文字色を指定すると、リセットさせるまでその色で表示し続けます。次のエスケープコードで文字色がデフォルトに戻ります。
\e[0m リセット
#include <stdio.h>
int main(int argc, char *argv[])
{
int c;
printf("\e[H\e[2J"); // スクリーンクリア
for (c = 0; c < 8; c++) {
printf("\e[3%dm", c); // 色変更(8色)
printf("%03d ", c);
printf("\e[0m"); // 色リセット
}
putchar('\n');
for (c = 8; c < 16; c++) {
printf("\e[9%dm", c - 8); // 色変更 (16色)
printf("%03d ", c);
printf("\e[0m"); // 色リセット
}
putchar('\n');
for (c = 16; c < 256; ) {
printf("\e[38;5;%dm", c); // 色変更(256色)
printf("%03d ", c);
printf("\e[0m"); // 色リセット
if ((++c - 16) % 36 == 0)
putchar('\n');
}
putchar('\n');
for (c = 0; c < 8; c++) {
printf("\e[4%dm", c); // 反転色変更(8色)
printf("%03d ", c);
printf("\e[0m"); // 色リセット
}
putchar('\n');
for (c = 8; c < 16; c++) {
printf("\e[10%dm", c - 8); // 反転色変更 (16色)
printf("%03d ", c);
printf("\e[0m"); // 色リセット
}
putchar('\n');
for (c = 16; c < 256; ) {
printf("\e[48;5;%dm", c); // 反転色変更(256色)
printf("%03d ", c);
printf("\e[0m"); // 色リセット
if ((++c - 16) % 36 == 0)
putchar('\n');
}
getchar();
printf("\e[H\e[2J"); // スクリーンクリア
return 0;
}
強調・斜体・アンダーライン
強調・斜体・アンダーラインの文字修飾を指定します。
\e[7m 強調文字開始
\e[27m 強調文字終了
\e[3m 斜体文字開始
\e[23m 斜体文字終了
\e[4m アンダーライン開始
\e[24m アンダーライン終了
それぞれの文字修飾を開始すると終了コードが出力されるまで適用され続けます。
それぞれの文字修飾は、斜体+アンダーラインのように重ねて指定できます。
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("\e[H\e[2J"); // スクリーンクリア
printf("\e[7m"); // 強調モード
printf("1234");
printf("\e[27m"); // 強調モードOFF
printf("\e[3m"); // 斜体モード
printf("5678"); //
printf("\e[23m"); // 斜体モードOFF
printf("\e[4m"); // アンダーライン開始
printf("ABCD"); //
printf("\e[24m"); // アンダーライン終了
putchar('\n');
printf("\e[3m\e[4m"); // 斜体とアンダーライン
printf("ABCDEFG");
printf("\e[23m\e[24m"); // 斜体OFF・アンダーライン終了
printf("HIJK");
getchar();
return 0;
}
「強調」は反転文字になります。
太字
文字を太字で出力します。
\e[1m 太字文字開始
\e[0m リセット
太字文字の開始を出力すると、設定リセットまで適用され続けます。太字の場合のリセットは文字色のリセットと同じコードです。
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("\e[H\e[2J"); // スクリーンクリア
printf("\e[1m"); // 太字モード
printf("1234");
printf("\e[0m"); // 太字モードの取り消し
printf("5678");
getchar();
return 0;
}
点滅
文字を点滅表示します。文字が消えたり表示したりを繰り返します。
\e[5m 太字文字開始
\e[0m リセット
点滅の開始を出力すると、設定リセットまで適用され続けます。点滅の場合のリセットは文字色のリセットと同じコードです。
#include <stdio.h>
int main(int argc, char *argv[])
{
printf("\e[H\e[2J"); // スクリーンクリア
printf("\e[5m"); // 点滅モード
printf("1234");
printf("\e[0m"); // モード取り消し
printf("5678"); // 12345678と表示され、1234だけが点滅する
getchar();
printf("\e[H\e[2J"); // スクリーンクリア
return 0;
}
スクロール
画面スクロールします。画面に表示されている文字の全体を行の上下に移動します。最初に2行単位でスクロールバック
\e[rT 下方向スクロール
\e[rS 上方向スクロール
r=行数
下の例は20行の文字が表示されている状態から、最初に2行ごとに5回上にスクロールして、続けて2行ごとに5回下にスクロールします。スクロールにより消滅した行は、逆方向にスクロールして戻しても再表示はしません。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i;
printf("\e[H\e[2J"); // スクリーンクリア
for (i = 0; i < 20; i++)
printf("%03d xxxxxxxx%02x\n", i, i);
printf("\e[H"); // カーソルホームに移動
for (i = 0; i < 5; i++) {
printf("\e[2S"); // 2行スクロールバック
fflush(stdout);
sleep(1);
}
for (i = 0; i < 5; i++) {
printf("\e[2T"); // 2行スクロール
fflush(stdout);
sleep(1);
}
getchar();
printf("\e[H\e[2J"); // スクリーンクリア
return 0;
}
010 xxxxxxxx0a
011 xxxxxxxx0b
012 xxxxxxxx0c
013 xxxxxxxx0d
014 xxxxxxxx0e
015 xxxxxxxx0f
016 xxxxxxxx10
017 xxxxxxxx11
018 xxxxxxxx12
019 xxxxxxxx13
スクロール行範囲の指定
スクロールする行範囲を設定します。画面を上下分割したり、最下段のみコマンドラインとして別扱いにしたい場合などに活用できます。
\e[n;mr スクロール範囲の設定
n=開始行 m=終了行
\e[r 領域設定のリセット
次の例では、1〜4行範囲だけが上に2回スクロールバックしてして、2行スクロールして戻ります(上2行はスクロールバックで消えます)。
最後に領域設定のリセットを行わないと、シェルプロンプトに戻った後もスクロール範囲が適用され続けるので注意が必要です。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i;
printf("\e[H\e[2J"); // スクリーンクリア
for (i = 0; i < 20; i++)
printf("%03d xxxxxxxx%02x\n", i, i);
fflush(stdout);
sleep(1);
printf("\e[1;4r"); // 1行(先頭行)〜4行目をスクロール領域に設定
printf("\e[H"); // カーソルホームに移動
for (i = 0; i < 2; i++) {
printf("\e[1S"); // 1行スクロールバック
fflush(stdout);
sleep(1);
}
for (i = 0; i < 2; i++) {
printf("\e[1T"); // 1行スクロール
fflush(stdout);
sleep(1);
}
getchar();
printf("\e[r"); // スクロール領域リセット
printf("\e[H\e[2J"); // スクリーンクリア
return 0;
}
002 xxxxxxxx02
003 xxxxxxxx03
004 xxxxxxxx04
005 xxxxxxxx05
006 xxxxxxxx06
007 xxxxxxxx07
008 xxxxxxxx08
009 xxxxxxxx09
010 xxxxxxxx0a
011 xxxxxxxx0b
012 xxxxxxxx0c
013 xxxxxxxx0d
014 xxxxxxxx0e
015 xxxxxxxx0f
016 xxxxxxxx10
017 xxxxxxxx11
018 xxxxxxxx12
019 xxxxxxxx13
削除(カーソル位置・行)
カーソル位置の文字、あるいは行を削除します。
\e[P カーソル位置の文字を削除
\e[M カーソル行を削除
削除した文字や行の間は前に詰められます。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i;
printf("\e[H\e[2J"); // スクリーンクリア
for (i = 0; i < 5; i++)
printf("%03d 123456789\n", i);
fflush(stdout);
sleep(1);
printf("\e[1;5H"); // r1-c5に移動
printf("\e[P"); // 現在文字削除
printf("\e[P"); // 現在文字削除
fflush(stdout);
sleep(1);
printf("\e[3;1H"); // カーソルをrow=3へ移動
printf("\e[M"); // 行削除
printf("\e[M"); // 行削除
fflush(stdout);
getchar();
return 0;
}
000 3456789
001 123456789
004 123456789
削除(行頭 行末 画面末)
カーソルの位置を基準に、行頭まで、行末まで、画面末までを削除します。
\e[1K 行頭まで削除
\e[K 行末まで削除
\e[J 画面末まで削除
行末まで削除の改行はそのまま残ります。次の行が連結されるわけではありません。
画面末までクリアというのは、カーソルの位置から右とその下すべての行を削除します。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i;
printf("\e[H\e[2J"); // スクリーンクリア
for (i = 0; i < 5; i++)
printf("%03d xxxxxxxx%02x\n", i, i);
fflush(stdout);
printf("\e[H"); // カーソルホームに移動
printf("\e[1;5H"); // r1-c5に移動
sleep(5);
printf("\e[1K"); // 行頭までクリア(カーソル位置もクリア)
fflush(stdout);
printf("\e[1B"); // 下移動
sleep(5);
printf("\e[K"); // 行末までクリア(カーソル位置もクリア)
fflush(stdout);
printf("\e[1B"); // 下移動
sleep(5);
printf("\e[J"); // 画面末までクリア(カーソル位置から行末も含め)
getchar();
return 0;
}
①行頭まで ②行末まで ③画面末まで
xxxxxxx00 xxxxxxx00 xxxxxxx00
001 xxxxxxxx01 001 001
002 xxxxxxxx02 002 xxxxxxxx02 002
003 xxxxxxxx03 003 xxxxxxxx03
004 xxxxxxxx04 004 xxxxxxxx04
空白挿入・行挿入
カーソル位置に空白、空行を挿入します。
\e[n@ カーソル位置の前に空白挿入
\e[nL カーソル行の前に行挿入
n=挿入する数
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int i;
printf("\e[H\e[2J"); // スクリーンクリア
for (i = 0; i < 3; i++)
printf("%03d 123456789\n", i);
fflush(stdout);
printf("\e[1;5H"); // r1-c5に移動
sleep(2);
printf("\e[2@"); // 2カラム挿入
fflush(stdout);
sleep(2);
printf("\e[1B"); // 下移動
printf("\e[2L"); // 2行挿入
fflush(stdout);
getchar();
printf("\e[H\e[2J"); // スクリーンクリア
return 0;
}
000 123456789
001 123456789
002 123456789
キーボード入力を直接取得するRAWモード
Linuxなどでは、キーボードの押下をEnterなしで入力検知できるような簡単なC言語のライブラリ関数がありません。
テキストエディタのようにキーボード操作を伴うものをエスケープコードと組み合わせて作るならば、キー押下イベントと押されたキーの判別ができるしくみが必要です。それは、やや面倒ですが、C言語ならばtermiosライブラリによる端末設定の変更で実現できます。
以下の例は、termiosによりキー入力を「RAWモード」に変更します。
#include <stdio.h>
#include <termios.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
unsigned char c;
struct termios raw;
struct termios org;
tcgetattr(STDIN_FILENO, &org);
raw = org;
raw.c_iflag &= ~(IXON);
raw.c_lflag &= ~(ICANON | ECHO | ISIG | VSUSP);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); // 入力をRAWモードに移行
while (1) {
// キーの読み取り
read(STDIN_FILENO, &c, 1);
if (c == 3) { // CTRL+C(=ASCII コードで3)
printf("CTRL+C exit\n");
break;
} else {
printf("INPUTKEY %c=(%02x)\n", c, c);
}
}
tcsetattr(STDIN_FILENO, TCSAFLUSH, &org); // 通常に戻す
}
RAWモードでは、read()で押下したキーのコードがそのまま標準入力から得られます。例として、以下のようにキー押下に対するバイトデータが得られます。
INPUTKEY a=(61) a
INPUTKEY b=(62) b
INPUTKEY =(20) スペース
INPUTKEY Enter
=(0a)
INPUTKEY ?=(e3) あ
INPUTKEY ?=(81)
INPUTKEY ?=(82)
INPUTKEY =(06) CTRL+F
INPUTKEY =(7f) BackSpace
INPUTKEY (1b) DELキー
INPUTKEY [=(5b)
INPUTKEY 3=(33)
INPUTKEY ~=(7e)
INPUTKEY (1b) PageDown
INPUTKEY [=(5b)
INPUTKEY 6=(36)
INPUTKEY ~=(7e)
INPUTKEY (1b) F1
INPUTKEY O=(4f)
INPUTKEY P=(50)
INPUTKEY (1b) ALT+N
INPUTKEY n=(6e)
CTRL+C exit
キーボードの異なるキー押下の組み合わせが重複するものがあります。例えばCTRL+MはEnterと同じ0x0aが得られます。
ターミナルのスクリーンサイズの取得方法
VT100エスケープコードでターミナルスクリーンの座標を基準に表示するようなプログラムを実装するならば、現在のスクリーンサイズを知らなければなりません。端末のサイズはioctl()にTIOCGWINSZを指定して取得できます。
マウス等によるターミナルのウィンドウサイズの変更にリアルタイムに追従したい場合は、SIGWINCHシグナルを捕捉して、シグナルハンドラ内でウィンドウサイズを取得すればよいでしょう。
それらを合わせて実装すると次のようになります。
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/ioctl.h>
#include <unistd.h>
// シグナルハンドラ関数
void handle_sigwinch(int sig)
{
struct winsize w;
// 端末画面サイズを取得
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) == 0) {
printf("terminal size changed: %d rows, %d columns\n", w.ws_row, w.ws_col);
} else {
perror("ioctl");
}
}
int main()
{
signal(SIGWINCH, handle_sigwinch);
while (1) {
pause(); // シグナルが来るまで待機
}
return 0;
}
terminal size changed: 24 rows, 134 columns
terminal size changed: 23 rows, 133 columns
terminal size changed: 23 rows, 132 columns
terminal size changed: 23 rows, 130 columns
terminal size changed: 22 rows, 128 columns
terminal size changed: 22 rows, 127 columns
terminal size changed: 22 rows, 126 columns