Curses for Ruby

※全文無料で公開しています。

CUIのアプリケーションを作るときによく利用されるのが、Cursesというライブラリ。
Rubyの場合、準標準ともいえるcursesライブラリがあり(※1.9.3までは標準添付ライブラリ)、これを使うのが一般的と思うのですが、Web上に解説がほとんどなく、癖もなかなか強いため、いざ使おうとするとかなり骨が折れます。

そこで、このエントリではcursesライブラリについてサンプルコードを交えながら説明していきたいと思います。

前提

このエントリは、以下の前提で書かれています。

・Rubyの基本的な文法を理解している。
・基本的なコマンドライン操作が出来る。

動作確認はMacでRuby 2.0.0を使ってやっています。
(それ以外の環境でもCursesが使えるなら問題なく動くとは思います。)

準備

Ruby 2.0.0以上の場合、cursesライブラリは標準では添付されていないので、RubyGemsからインストールする必要があります。

コマンドラインから以下のコマンドを実行してインストールします。
$はコマンドプロンプト。以下、同様)

$ gem install curses

Cursesの動作確認

サンプルコードを動かして、cursesの動作確認をしてみましょう。

Gistから以下のコードをダウンロードして、コマンドラインから実行して下さい。

[コード]
hello.rb

[実行例]
$ chmod 755 hello.rb
$ ./hello.rb

Cursesが使える環境で、cursesライブラリが正しくインストールされていれば、問題なく実行出来るはずです。

上記のスクリプトを実行すると、ターミナルの中央に"Hit any key"という文字が表示されます。
そのメッセージに従ってキーボードの任意のキーを叩くと、"Hit any key"という文字は消え、四角い枠の中に"Hello, World!"と表示されるかと思います。
そして、さらにキーボードの任意のキーを叩くと、スクリプトは終了します。

サンプルコードの簡単な説明

本格的な説明の前に、サンプルコードを簡単に見ておきます。

[4行目]
cursesライブラリを読み込んでいます。

[5行目]
cursesライブラリの多くの機能は、Cursesモジュールのモジュール関数として実装されています。
なので、これらのモジュール関数を簡単に呼べるようにしています。

[7 - 16行目]
ウィンドウを作成し、引数で指定されたmessageを表示して入力待ちになり、何か一文字でも入力されたらウィンドウを閉じるメソッドを定義しています。
ここでいうウィンドウはGUIのウィンドウとは少し違います。
ウィンドウの詳細については後述します。

[18行目]
cursesライブラリの初期化を行っています。
cursesライブラリを使う場合、まず初期化しておく必要があります。

[20行目]
入力モードをcbreakモードにしています。
入力モードの詳細については後述します。

[22 - 24行目]
"Hit any key"をターミナルの中央に表示しています。

[25行目]
文字の入力待ちをしています。

[26 - 27行目]
7 - 16行目で定義されたshow_messageメソッドを呼び出して、ウィンドウに"Hello, World!"を表示しています。

[29行目]
cursesライブラリの終了処理を行っています。
cursesライブラリを使い終わったら、呼び出すべきです。

———

以上から、cursesライブラリを使うときのおおざっぱな流れは、

1. cursesライブラリの読み込み
2. cursesライブラリの初期化
3. ウィンドウを生成したり、文字を表示したり、入力を受け取ったりする
4. cursesライブラリの終了処理

となっていることが分かるかと思います。

cursesライブラリの初期化・終了処理

cursesライブラリを使う場合、まず初期化が必要となります。
cursesライブラリを初期化するには、次のモジュール関数を呼びます。

Curses.#init_screen()
cursesライブラリを初期化します。

そして、cursesライブラリを使い終わったら、以下のモジュール関数を呼びます。

Curses.#close_screen()
cursesライブラリの終了処理を行います。

cursesライブラリを使えるようにすると、ターミナルの画面のサイズを知ることが出来ます。
画面のサイズを知るには、以下のモジュール関数を使います。

Curses.#lines()
画面の行数を返します。

Curses.#cols()
画面の列数を返します。

Cursesの構成要素

Cursesは以下の要素から構成されています。
(※いくつかの概念は、分かりやすくするために自分が追加しています)

・ウィンドウ
・仮想画面
・画面
・カラーパレット
・カラーペアパレット

ウィンドウとは、画面に出力する文字情報を持った矩形の領域です。
領域のサイズと画面に対する位置を持っていて、Curses::Window#noutrefreshメソッドやCurses::Window#refreshメソッドによって、ウィンドウの内容は仮想画面(と画面)に出力されます。

仮想画面とは、画面への出力を効率よく行うための仮想的な画面です。
ちょっと前のGUIプログラミングをやったことがある人なら、ダブルバッファリングという言葉になじみがあるかと思います。
そのバッファに相当するものが仮想画面です。

画面とは、実際に文字情報が出力される画面です。
ターミナルの画面そのものです。

カラーパレットとは、画面に文字を出力するときに使える色を指定するパレットです。
0は黒、1は赤、2は緑、・・・といったふうに、何番が何色という対応付けを行います。

カラーペアパレットとは、画面に文字を出力するときの前景色と背景色のペアを指定するパレットです。
前景色と背景色は、カラーパレットの番号で指定することになります。
例えば、カラーパレットの0が黒、カラーパレットの1が赤の場合に、カラーペアパレットの1を[0, 1]にし、カラーペアパレットの1を使って文字列を出力すると、その文字列は、前景色が黒、背景色が赤で表示されることになります。

以下では、それぞれの要素について、詳細をみていきます。

ウィンドウの生成・削除

ウィンドウは、画面に出力する文字情報を持った矩形の領域です。
Cursesでは、ウィンドウに対して文字を追加・削除する操作を行い、その変更を仮想画面、そして画面へと反映させていきます。

Cursesではウィンドウを複数作ることが出来ます。
また、初期化した段階で、画面全体を覆うデフォルトのウィンドウが作られています。
Cursesモジュールのモジュール関数のいくつかは、このデフォルトのウィンドウを対象としたものです。

デフォルトのウィンドウを取得するには、以下のモジュール関数を使います。

Curses.#stdscr()
画面全体を覆うデフォルトのウィンドウを返します。
デフォルトのウィンドウは、Curses::Windowクラスのインスタンスです。

また、ウィンドウを新しく生成したい場合、次のクラスメソッドを使います。

Curses::Window.new(height, width, top, left)
高さがheight行、幅がwidth列で、左上の位置が画面のtop行目、left列目であるような新しいウィンドウが生成し、そのインスタンスを返します。
(画面の左上が0行0列目)

生成したウィンドウを削除するには、以下のメソッドを使います。

Curses::Window#close()
ウィンドウを削除し、メモリを解放します。

ウィンドウへの文字の追加・削除

各ウィンドウにはカーソルがあり、その位置に対して文字を追加・削除することが出来ます。

ウィンドウ内でのカーソル位置を知るには、以下のメソッドを使います。

Curses::Window#cury()
カーソルがウィンドウの何行目にあるかを返します。
(ウィンドウの左上が0行目)

Curses::Window#curx()
カーソルがウィンドウの何列目にあるかを返します。
(ウィンドウの左上が0列目)

カーソル位置を移動するには、次のメソッドを使います。

Curses::Window#setpos(y, x)
カーソル位置をウィンドウのy行x列目にします。
(ウィンドウの左上が0行0列目)

ウィンドウに文字を追加するには、以下のメソッドを使うことが出来ます。

Curses::Window#addch(ch)
ウィンドウのカーソル位置に文字chを上書きし、カーソルを進めます。

Curses::Window#addstr(str)
Curses::Window#<<(str)
ウィンドウのカーソル位置に文字列strを上書きし、カーソルを進めます。

Curses::Window#insch(ch)
ウィンドウのカーソル位置に文字chを挿入します。

Curses::Window#insertln()
ウィンドウのカーソル位置に一行挿入します。

ウィンドウの文字を削除するには、以下のメソッドを使うことが出来ます。

Curses::Window#delch()
ウィンドウのカーソル位置の文字を削除します。
(以降の文字は前に詰められます)

Curses::Window#deleteln()
ウィンドウのカーソル位置の行を削除します。
(以降の行は前に詰められます)

Curses::Window#clear()
ウィンドウの内容をすべて削除します。

Curses::Window#clrtoeol()
ウィンドウのカーソル位置から行末までを削除します。

ウィンドウには、枠をつけることも可能です。

Curses::Window#box(vert, hor)
ウィンドウの矩形領域の一番外側を枠で囲います。
このとき、垂直方向には文字vert、水平方向には文字horが使われます。

ウィンドウに枠をつけるときに気をつけたいのが、この枠は矩形領域の外側につけられるわけではなく、矩形領域の内側につけられるということです。
このため、枠を作る文字の下にすでに文字があると上書きされてしまいますし、逆もまた然りとなります。

———

ここまでで、ウィンドウに対して文字の追加や削除を行う方法について述べてきました。
しかし、これらの変更はそれだけでは画面には反映されません。

次は、ウィンドウに対する変更を画面に反映する方法について、説明してきます。

ウィンドウの変更の画面への反映

ウィンドウの変更を仮想画面(と画面)へ反映するには、以下のメソッドを使います。

Curses::Window#noutrefresh()
ウィンドウの変更を仮想画面へ反映します。
(画面へは反映されません)

Curses::Window#refresh()
ウィンドウの変更を仮想画面と画面へ反映します。

Curses::Window#noutrefreshメソッドを使った場合、仮想画面の内容を画面に反映する必要があります。
その場合、次のモジュール関数を使います。

Curses.#doupdate()
仮想画面の内容を画面へ反映します。

ウィンドウが一つしかない場合、refreshを使うのとnoutrefresh + doupdateを使うのは同じですが、ウィンドウが複数あった場合、効率が変わってきます。
一般に、デバイスへのアクセスはメモリへのアクセスより遅いため、頻繁にデバイスへアクセスするのは効率が悪くなります。
そのため、複数のウィンドウを使う場合、各ウィンドウに対してnoutrefreshをしたあとで最後に一度だけdoupdateをする方が、効率がよくなります。

なお、複数のウィンドウがある場合、Cursesでは各ウィンドウがどの順番に重なっているのかは管理していません。
各ウィンドウの変更内容は、noutrefresh(もしくはrefresh)された順に仮想画面へ上書きされていきます。
そのため、見かけ上は、あとでnoutrefreshされたウィンドウの方が上に重なっているように見えます。

———

ここまでの内容の確認と、refresh / noutrefresh + doupdateの違いを見るために、次のコードを見てみます。

[コード]
multi_window.rb

[6 - 11行目]
オープンクラスを使って、タッチ更新を行うメソッドを定義しています。
タッチ更新については後述します。

[16行目]
画面全体を覆うデフォルトのウィンドウを取得しています。

[18 - 26行目]
複数のウィンドウを、位置を少しずつズラしながら生成しています。
各ウィンドウには"Hello"という文字列と枠を追加しています。

[30 - 32行目]
デフォルトのウィンドウの左上に"refresh"という文字列を追加し、画面に表示しています。
この時点では、18〜26行目で作ったウィンドウはまだ表示されません。

[35 - 38行目]
各ウィンドウを、0.1秒間隔でrefreshして画面に表示しています。
・refreshされるたびに画面に変更が反映されること
・あとからrefreshされたウィンドウが上に重なったいるように見えること
が確認出来るかと思います。

[43 - 46行目]
デフォルトのウィンドウを全部クリアして、ウィンドウの左上に"noutrefresh + doupdate"という文字列を追加し、画面に表示しています。
35〜38行目で表示されたウィンドウは、デフォルトのウィンドウの変更が反映されることで、消えたように見えます。(※デフォルトのウィンドウは画面全体を覆っていることに注意)

[49 - 54行目]
各ウィンドウをタッチ更新(後述)したあと、0.1秒間隔でnoutrefreshして仮想画面に反映し、最後にdoupdateで画面に表示しています。
・noutrefreshしただけでは、画面に反映されないこと
・doupdateしたときに、それまでの変更がすべて画面に反映されること
が確認出来るかと思います。

ウィンドウの位置とサイズ

ウィンドウは、位置を変えたりサイズを変更したりすることも可能です。

まず、ウィンドウの位置を取得するには、以下のメソッドが使えます。

Curses::Window#begy()
ウィンドウの左上が、画面の何行目にあるかを返します。
(画面の左上が0行目)

Curses::Window#begx()
ウィンドウの左上が、画面の何列目にあるかを返します。
(画面の左上が0列目)

ウィンドウのサイズを取得するには、以下のメソッドが使えます。

Curses::Window#maxy()
ウィンドウの行数を返します。

Curses::Window#maxx()
ウィンドウの列数を返します。

ウィンドウの位置を変えるには、次のメソッドを使います。

Curses::Window#move(top, left)
ウィンドウの左上がtop行left列目になるように移動します。
(画面の左上が0行0列目)

そして、ウィンドウのサイズを変えるには、次のメソッドを使います。

Curses::Window#resize(height, width)
ウィンドウのサイズをheight行width列に変更します。

ウィンドウのタッチ更新

ところで、実際にウィンドウの移動を行ってみようとすると、不思議なことが起こります。

次のコードを実行してみて下さい。

[コード]
bad_move_1.rb

やろうとしていることは、ウィンドウを生成して画面に表示したあと、ウィンドウを移動して再度表示させるということです。

ただ、実際にやってみると、移動前のウィンドウが画面に残ってしまっています。
これは、ウィンドウそのものが画面に表示されているわけではなく、仮想画面の内容が画面に表示されているからです。

すなわち、一度ウィンドウの内容を仮想画面に反映して画面に表示したとき、仮想画面にはウィンドウが0行0列目にある状態が書き込まれます。
そのあと、ウィンドウを2行2列目に移動して仮想画面に反映すると、仮想画面にはウィンドウが2行2列目にある状態が書き込まれるだけなので、元々の0行0列目にあるウィンドウのイメージは残ったままになります。
そのため、移動前のウィンドウが画面に残ってしまうわけです。

それなら、画面全体を覆うデフォルトのウィンドウを仮想画面に反映し、そのあと移動後のウィンドウを仮想画面に反映してみてはどうでしょうか。
これなら、デフォルトのウィンドウを仮想画面に反映した時点で仮想画面全体がまっさらになり、問題なく移動後のウィンドウのみが表示されそうです。

次のコードを試してみましょう。

[コード]
bad_move_2.rb

先ほどとの違いは18行目で、デフォルトのウィンドウを仮想画面と画面に反映しようとしています。

しかし、実際に動かしてみると分かりますが、結果は先ほどと変わりません。
移動前のウィンドウが画面に残ってしまいます。
これはなぜでしょうか?

原因は、noutrefresh(もしくはrefresh)は「ウィンドウに対する変更」を仮想画面(と画面)に反映するので、ウィンドウに何も変更が加わっていない場合、何も仕事をしないからです。

今回の場合、本当はデフォルトのウィンドウ全体を仮想画面に反映したいわけですが、デフォルトのウィンドウには何も変更が加えられていないため、refreshをしても仮想画面には何も影響が出ないのです。

これを解決するには、いくつかの方法があります。

例えば、デフォルトのウィンドウをクリアしてあげれば、デフォルトのウィンドウ全体が更新されたという扱いになるので、移動前のウィンドウは表示されなくなります。
multi_window.rbの43行目で行の削除ではなくクリアをしているのは、このためです。

あるいは、移動前のウィンドウを一度クリアして仮想画面に反映したあと、移動してから再度ウィンドウの内容を作って仮想画面に反映するということも出来ます。

しかし、いずれの方法もウィンドウの内容を消したくない場合には不都合です。

そこで使うのが、ウィンドウのタッチ更新です。
これは、ウィンドウの内容を変えることなく、ウィンドウ全体を更新したと見做させることです。
ファイルの内容を変えることなくタイムスタンプだけを更新する、touchコマンドを想像すると分かりやすいかと思います。

C言語の場合、touchwin()という関数が用意されているのですが、cursesライブラリではタッチ更新のインタフェースが用意されていません。
そこで、ちょっとした工夫が必要になります。

ウィンドウを「今の位置」に移動させることで、ウィンドウ全体を更新したと見做させることが出来ます。

もちろん、今の位置に移動させるだけなので、ウィンドウの内容も位置も実際には変化しません。
しかし、更新されたと見做されるので、noutrefreshやrefreshによってウィンドウ全体の内容が仮想画面に反映されるようになります。

次のコードを試してみて下さい。

[コード]
good_move.rb

20行目で、デフォルトのウィンドウをタッチ更新しています。
これにより、移動前のウィンドウが画面に残らないようになります。

実際には、multi_window.rbのようにオープンクラスでCurses::WindowクラスにCurses::Window#touchというメソッドを定義しておくといいかと思います。

文字の入力と入力モード

ここまでは出力の話でした。
次は、入力の話をしていきます。

通常、入力はバッファリングされ、Enterキーを押された時点で初めてユーザプログラムにはデータが渡されます。
しかし、それではCUIアプリケーションを作るには不都合です。
そこで、Cursesでは入力モードが用意されています。

入力モードには、以下の3つがあります。

・cookedモード
・cbreakモード
・rawモード

cookedモードは、通常の入力と同じように、バッファリングを行う状態です。
Enterキーが押されるまでは、入力されたデータはユーザプログラムに渡ってきません。

cbreakモードは、バッファリングを行わない状態です。
Curses.#getch()モジュール関数などを呼んだときに、入力された文字は即座にユーザプログラムに渡されます。

rawモードは、バッファリングを行わず、さらに通常はshellで解釈される特殊な文字(Ctrl+CやCtrl+Zなど)をユーザプログラム側で扱えるようにします。
このモードの場合、Ctrl+Cを押してもプログラムは強制終了しなくなるので、ユーザプログラム側で適切にハンドルする必要があります。

入力モードを切り替えるには、以下のモジュール関数を使用します。

Curses.#nocbreak()
Curses.#nocrmode()
Curses.#noraw()
入力モードをcookedモードにします。

Curses.#cbreak()
Curses.#crmode()
入力モードをcbreakモードにします。

Curses.#raw()
入力モードをrawモードにします。

ユーザからの入力を得るには、以下のメソッドを使います。

Curses::Window#getch()
ユーザの入力から一文字読み込んで返します。

Curses::Window#getstr()
ユーザの入力から一行読み込んで返します。
(cbreakモードやrawモードでも、Enterキーが押されて一行になるまではブロッキングされます)

関連するメソッドで、以下のようなものもあります。

Curses::Window#ungetch(ch)
文字chをバッファの先頭に戻します。
戻せるのは一文字までです。

Curses::Window#inch()
カーソル位置の文字を読み込んで返します。

ユーザからの入力を得るメソッドがウィンドウのインスタンスメソッドになっているのは、入力のエコーバックの関係です。
ユーザが文字を入力すると、基本的にはウィンドウのカーソルの位置にエコーバックされます。

エコーバックするかどうかは、以下のモジュール関数で設定することが出来ます。

Curses.#echo()
入力のエコーバックを有効にします。

Curses.#noecho()
入力のエコーバックを無効にします。

なお、次のモジュール関数を使うことで、カーソルの表示/非表示を切り替えることも出来ます。

Curses.#curs_set(visibility)
visibilityが0の場合、カーソルが非表示になります。
visibilityが1の場合、カーソルが表示されます。

カーソルキーやファンクションキーの入力を受けたい場合、そのままでは扱えません。
この場合、以下のメソッドを呼びだしておく必要があります。

Curses::Window#keypad(bool)
boolがtrueの場合、キーパッドが有効になります。
すなわち、カーソルキーやファンクションキーの入力が、Curses::KEY_*という定数で返ってくるようになります。
(これらの定数については、RDocなどを参照して下さい)

入力待ちのタイムアウト

getchなどでは、ユーザから入力があるまでプログラムは停止して待つことになります。
この状態をブロッキングと呼びます。
一方、プログラムは停止せず、入力がなかった場合にはエラーを返して戻る状態をノンブロッキングと呼びます。

ユーザから入力がないときにいつまでも待ち続けてしまうと困る場合、入力待ちのタイムアウトが必要になります。

入力待ちのタイムアウトを設定するには、次のメソッドを使います。

Curses::Window#timeout=(val)
ウィンドウの入力待ちについて、タイムアウトを設定します。
・valが負の値の場合、ブロッキングされます。
・valが0の場合、ノンブロッキングになります。
・valが正の値の場合、入力を最大valミリ秒待つようにします。

上記のメソッドの場合、タイムアウトするまでの時間を設定しているので、それよりも前に入力があった場合には即座にユーザプログラムに制御が戻ってきます。
ただ、場合によっては、ユーザからの入力があってもなくても常に一定時間待って、そのあと処理を行いたいということもあるかと思います。
そういった場合には、timeoutライブラリを使うといいかもしれません。

timeoutライブラリを読み込むと、KernelモジュールにKernel#timeoutメソッドが追加で定義されます。
このtimeoutメソッドは、KernelモジュールがObjectクラスにincludeされているので、任意の場所から関数のように呼び出すことが出来ます。

Kernel#timeout(sec) {|i| ...}
ブロックの内容を最大sec秒実行します。
secには小数も指定出来ます。
指定された時間を越えた場合、TimeoutError例外が発生し、ブロックを抜けます。

例えば、次のようにすることで常に一定時間待つようにすることが出来ます。

input = nil
begin
  timeout(待つ秒数) do
    input = Curses.getch
    sleep
  end
rescue TimeoutError
  # ignore
end

———

ユーザから入力を受け付けたり、入力待ちのタイムアウトの例として、次のコードを書いてみました。

[コード]
input_dialog.rb

実行すると、文字列の入力が促されます。
このとき、'exit'もしくは'quit'と入力してEnterキーを押すと、プログラムは終了します。
それ以外の文字列が入力された場合、その文字列が画面内でバウンドして表示されます。
'q'が入力されると文字列のバウンドはすぐに終わり、新しい文字列の入力が促されます。

このコードについて、少し説明します。

[39 - 60行目]
ユーザからの入力を受け取るための特異メソッドを定義しています。
このとき、
・ユーザから入力を受けるときは、カーソルを表示し、エコーバックする
・ユーザの入力が終わったら、カーソル非表示、エコーバックなしにする
としています。
エコーバックがあると、カーソル位置の文字が上書きされてしまいます。
そこで、ユーザの入力が終わったら、上書きされた部分をクリアしています。
また、ユーザの入力が長かった場合、ウィンドウの枠が上書きされてしまうことがあるので、枠の修復も行っています。

[75 - 113行目]
ユーザから入力された文字列を、画面内でバウンドするように描画しています。
このとき、描画は一定間隔毎に行う必要があるので、ユーザからの入力があるかないかに関わらず、一定時間入力待ちをするようにしています。
(そうしないと、キーを押しっぱなしにされた場合、描画間隔が短くなってしまいます)

———

基本的な内容はここまでなのですが、出力文字を修飾したり、色をつけたい場合もあるかと思うので、次は出力文字の修飾について説明してきます。

出力文字の修飾

出力する文字に修飾をつけたい場合、属性を指定した状態で文字の追加を行います。

属性はCurses::A_*という定数として定義されています。
属性のそれぞれは特定のビットが立った整数で、複数の属性を同時に指定したい場合、論理和をとって指定します。
(どのような属性があるのかは、RDocを参照して下さい)

属性を設定するには、以下のメソッドを使います。

Curses::Window#attron(attrs)
指定された属性attrsのビットを立てます。

Curses::Window#attroff(attrs)
指定された属性attrsのビットを下ろします。

Curses::Window#attrset(attrs)
属性を指定された属性attrsにします。

また、強調表示(反転して表示)は専用のメソッドが用意されています。

Curses::Window#standout()
これ以降、ウィンドウに追加される文字は強調表示で修飾されます。

Curses::Window#standend()
ウィンドウに追加される文字を強調表示で修飾するのを止めます。

———

以下のコードは、出力文字の修飾のサンプルコードです。

[コード]
attr_sample.rb

[9 - 14行目]
今回試す属性をハッシュで用意しています。
それぞれ、通常(A_NORMAL)、強調表示(A_STANDOUT)、下線(A_UNDER_LINE)、反転(A_UNDERLINE)、点滅(A_BLINK)です。

[22 - 28行目]
それぞれの属性の値と、属性を設定して文字を追加したときの効果が確認出来るようにしています。
それぞれの属性は特定のビットが立った整数で、属性が設定された状態で追加された文字はその属性の修飾がつけられて出力されることが分かるかと思います。

カラーパレットとカラーペアパレット

出力文字に色をつける場合も、同様に属性を設定して出力することになります。

ただ、ここで少し分かりにくいのが、色をつける場合に指定する属性は、カラーペアパレットの番号に対応したものだということです。
出力された文字はカラーペアパレットの番号と紐付けられることになります。

一つずつ説明していきましょう。

まず、そもそもターミナルが色を扱えるものでないと、色をつけて出力することは出来ません。(当然ですね)
ターミナルが色を扱えるかは、次のモジュール関数で確認出来ます。

Curses.#has_colors?()
ターミナルが色を扱えるかを返します。

ターミナルで色を使う場合、最初に次のモジュール関数を呼んでおく必要があります。

Curses.#start_color()
色の初期化を行い、Cursesで色を扱えるようにします。

Cursesでは、カラーパレットによって色を管理しています。
これは、0は黒、1は赤、・・・といったふうに、数字と色を結びつけるものです。
(※厳密には違うのですが、そう考えると分かりやすいです)
ここではこの数字を色番号と呼ぶことにします。

カラーパレットのサイズは、次のモジュール関数で知ることが出来ます。

Curses.#colors()
カラーパレットのサイズを返します。
使える色番号は、0から(Curses.colors - 1)までとなります。

色番号に対応する色(RGB)を知りたい場合、次のモジュール関数を使います。

Curses.#color_content(c_idx)
色番号c_idxに対するRGBを配列[r, g, b]で返します。
r, g, bはそれぞれ0〜1000の整数で、赤、緑、青の度合いを示します。

基本的にはカラーパレットの色(RGB)を変えることは出来ないのですが、ターミナルによっては変えることも出来ます。

カラーパレットの色を変えることが出来るかは、次のモジュール関数で確認出来ます。

Curses.#can_change_color?()
カラーパレットの色を変えることが出来るかを返します。

カラーパレットの色を変えられる場合、次のモジュール関数で色を変えます。

Curses.#init_color(c_idx, r, g, b)
色番号c_idxのRGBを[r, g, b]にします。
(r, g, bは0〜1000の整数)

実際に出力する文字に色を指定する場合は、カラーペアを使います。
カラーペアは色番号のペアで、それぞれ前景色、背景色を意味します。
このカラーペアと数字を結びつけて管理するのが、カラーペアパレットです。
この数字をここではカラーペア番号と呼ぶことにします。

カラーペアパレットのサイズは、次のモジュール関数で知ることが出来ます。

Curses.#color_pairs()
カラーペアパレットのサイズを返します。
使えるカラーペア番号は、0〜(Curses.color_pairs - 1)となります。

カラーペア番号に対応する色番号のペアを知るには、次のモジュール関数を使います。

Curses.#pair_content(p_idx)
カラーペア番号p_idxに対応する色番号の配列[fg_idx, bg_idx]を返します。
fg_idxは前景色の色番号で、bg_idxは背景色の色番号です。

カラーペアを設定するには、次のモジュール関数を使います。

Curses.#init_pair(p_idx, fc_idx, bc_idx)
カラーペア番号p_idxに対応するカラーペアを[fg_idx, bg_idx]にします。
fg_idxは前景色の色番号、bg_idxは背景色の色番号です。
カラーペア番号には、1以上を指定出来ます。

ここで気をつけたいのが、Curses.#init_pairに指定出来るカラーペア番号は1以上ということです。
これは、カラーペア番号の0はデフォルトのカラーペアで固定されているからです。
デフォルトのカラーペアは、普通は[Curses::COLOR_WHITE, Curses::COLOR_BLACK]となります。

デフォルトのカラーペアとして、ターミナルのデフォルトの色を使いたい場合もあるかと思います。
その場合、次のモジュール関数を呼びます。

Curses.#use_default_colors()
ターミナルのデフォルトの色を色番号-1として使えるようにします。
そして、デフォルトのカラーペアを[-1, -1]にします。

出力する文字に色をつけるには、カラーペア番号に対応する属性を設定した状態で文字を追加します。

カラーペア番号に対応した属性を得るには、次のモジュール関数を使います。

Curses.#color_pair(p_idx)
カラーペア番号p_idxに対応する属性を返します。
この属性をCurses::Window#attrsetメソッドなどで指定した状態で文字を追加することで、指定されたカラーペアで出力されるようになります。

———

色を使うサンプルコードを見てみます。

[コード]
color_sample.rb

[8 - 9行目]
ターミナルで色を使えるようにし、デフォルトのカラーペアをターミナルのデフォルトの色を使うようにしています。

[28 - 41行目]
前景色と背景色の組合せをカラーペアパレットにセットし、そのカラーペアを使って文字列を出力しています。
カラーペア番号が0の場合は、デフォルトのカラーペアのままで変更されないことも分かるかと思います。

[49 - 62行目]
カラーペア番号1の前景色をコロコロ切り替えて再描画しています。
これにより、カラーペア番号1で出力された文字列の前景色がコロコロと切り替わります。
これは、文字列が色のペアと結びつけられて出力されているわけではなく、カラーペア番号と結びつけられて出力されているからです。
そのため、カラーペア番号の指すカラーペアの内容が変われば、文字列の色も変わっていきます。

最後に

説明はこれでオシマイです。
ここまで理解出来れば、いろいろなCUIアプリケーションを作れるかと思います。

なお、ここまでで説明してないこととしては、
・マウス入力
・ウィンドウのスクロール
・Curses::Padクラス
があります。
これらについてはRDocなどを参照して下さい。

参考になったら、購入してもらえると嬉しいです。

ここから先は

0字

¥ 100

この記事が気に入ったらサポートをしてみませんか?