読者です 読者をやめる 読者になる 読者になる

いものやま。

雑多な知識の寄せ集め

Curses for Ruby。(その6)

技術 Ruby 端末 Curses

昨日は文字の入力について説明した。

基本的な内容はこれで終わりなんだけど、出力する文字を修飾したり、色をつけたい場合もあるかと思うので、今日は出力する文字の修飾について説明していく。

出力文字の修飾

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

属性は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
#--------------------
# 出力する文字を修飾するサンプル
#====================

require 'curses'

Curses.init_screen

text = "TEXT"
attrs = {
  normal: Curses::A_NORMAL, 
  standout: Curses::A_STANDOUT,
  underline: Curses::A_UNDERLINE,
  reverse: Curses::A_REVERSE,
  blink: Curses::A_BLINK}

Curses.setpos(0, 0)
Curses.addstr('-' * 48)
Curses.setpos(1, 0)
Curses.addstr(sprintf("%-10s %-32s %s", "attribute", "value", "view"))
Curses.setpos(2, 0)
Curses.addstr('-' * 48)
attrs.each_with_index do |(name, attr), i|
  Curses.setpos(i + 3, 0)
  Curses.addstr(sprintf("%-10s %032b ", name.to_s, attr))
  Curses.attron(attr)
  Curses.addstr(text)
  Curses.attroff(attr)
end
Curses.setpos(attrs.size + 3, 0)
Curses.addstr('-' * 48)

Curses.refresh
Curses.getch

Curses.close_screen

実行すると、通常(A_NORMAL)、強調表示(A_STANDOUT)、下線(A_UNDERLINE)、反転(A_REVERSE)、点滅(A_BLINK)の各属性について、それぞれの属性の値と、属性を設定して出力された文字が表示される。

興味深いのは、点滅による修飾。
端末とエスケープシーケンスの話。 - いものやま。に書いた通り、プログラム側はエスケープシーケンスを送っているだけで、実際に点滅させているのは端末の方なので、プログラムが終わったあとも、点滅で修飾された文字列は点滅し続ける。
このあたりは、エスケープシーケンスを理解していないと、プログラムが終わったあとも文字列が点滅し続ける理由が理解できないと思う。

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

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

以下では一つずつ説明していく。

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

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_idxRGB[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以上を指定できる(=0は指定できない)。

ここで気をつけたいのが、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
#--------------------
# 出力する文字に色をつけるサンプル
#====================

require 'curses'
include Curses

init_screen
start_color
use_default_colors

setpos(0, 0)
if has_colors?
  addstr "Terminal has colors."
else
  addstr "Terminal does not have colors."
end

setpos(1, 0)
if can_change_color?
  addstr "Terminal can change color."
else
  addstr "Terminal can not change color."
end

lines = 2
rows = 6
p_idx = 0
colors.times do |fg_idx|
  setpos(lines + fg_idx, 0)
  attrset(A_NORMAL)
  addstr sprintf("fg_idx:%02d (%4d, %4d, %4d)", fg_idx, *color_content(fg_idx))

  colors.times do |bg_idx|
    attrset(A_NORMAL)
    addch " "
    init_pair(p_idx, fg_idx, bg_idx)
    attrset(color_pair(p_idx)|A_NORMAL)
    addstr "Hello" 
    p_idx = (p_idx + 1) % color_pairs
  end
end

refresh
getch

Curses.timeout = 0  # set non-blocking
bg_idx = pair_content(1).slice(1)
done = false
until(done)
  colors.times do |fg_idx|
    init_pair(1, fg_idx, bg_idx)
    refresh

    ch = getch
    unless ch.nil?
      done = true
      break
    end

    sleep 0.1
  end
end

close_screen

これを実行すると、ターミナルで色を使える環境であれば、まず、前景色と背景色の組合せで文字列が表に出力される。
そして、何かキーを押すと、カラーペア番号1の前景色がコロコロと切り替わって表示される。
さらに何かキーを押すと、プログラムは終了する。

カラーペアを設定して、そのカラーペア番号に対応する属性を設定して、文字列の出力を行うという一連の流れが分かると思う。
また、すでに出力された文字も、その文字に設定されたカラーペア番号の内容が変更されれば、その色も変わるということが分かると思う。


これで説明はオシマイ。

他、説明してないこととしては、以下のものがある:

  • マウス入力
  • ウィンドウのスクロール
  • Curses::Padクラス

これらについてはRDocなどを参照。

今日はここまで!