s-lang MLにポストしてたんですが、昨日まで書いた時点で
やはりrubyの専門家にも見ていただいたほうがいいかと
思い、こっちにもクロスポストしようと思います。

では前回までの:

-- 
人生を背負い投げ

菊谷 誠(Kikutani Makoto)  kikutani / sprintmail.com
Date: Sat, 27 Feb 99 05:56:24 +0900
Subject: [s-lang:00701] ruby で学ぶ S-Lang
 画面制御 
To: s-lang / juice.or.jp (s-lang ML)

きくたにです。昨日は変なメール出してすみません。

いや、最近このML止ってるかと思って(debian-usersとクロスポスト
したの届いてないですよね)テストしたのだけど、
開発中のruby/slangのメイラで出したら送信部分が腐っていたのでした。

さて、そのrubyのslang拡張モジュールですが

ftp://ftp.netlab.co.jp/pub/lang/ruby/contrib/slanglib-0.11.tar.gz

にあります。昨日でかいbugを見つけたのでバージョンは上るでしょうが。

しかしエンジニア(特にプログラマ?)共通の気質、

     「ドキュメントを書くのは大嫌いだ」

によって何も説明が入ってません。

けれど、メールを書くのは好きなので、ここで連載の形で説明したいと
思います。興味のない人ごめんなさい。

ruby-listとクロスにしようかとも思ったけど、(まつもとさんが
まだいるか知りませんが)前田さんが嘘をチェックしてくれるだろうから
こっちだけにします。

-- 
人生を背負い投げ

菊谷 誠(Kikutani Makoto)  kikutani / sprintmail.com

Date: Sat, 27 Feb 99 07:01:36 +0900
Subject: [s-lang:00702] Re: ruby で学ぶ S-Lang 
 画面制御 
To: s-lang / juice.or.jp (s-lang ML)

rubyのslang拡張モジュールは、SLsmg_ の関数をrubyから使えるようにした
ものです。たぶんこの接頭語は SLang Screen ManaGer かなんかの略。
これはslangのソースに含まれる cslang.txt というドキュメントに
解説されてます。

その気になればインタプリタ言語としてのS-Langを使えるようにも
できるけど、まあrubyから使う必要はあまりないでしょう。

そういうわけで、以後は画面制御の話だけになります。

[1] 仮想ディスプレイ

ディスプレイモデル非常に単純です。画面の左上を座標(0,0)とし、
SLsmg_gotorc で描きたい場所に行って文字(列)を書くと。

ではrubyでの簡単な例。

---------------------------------------------------------
require "slang"
include Slang

slang_init_terminal(1, 1)

slsmg_cls

slsmg_gotorc( (sltt_screen_rows-1)/2, (sltt_screen_cols-1)/2-1 )
slsmg_set_color SL_BG_WHITE+SL_RED
slsmg_write_string ">*<"
slsmg_set_color SL_BG_WHITE+SL_BLACK
slsmg_write_string "←ここが真中じゃよ"
slsmg_refresh
slkp_getkey

sl_reset
exit
---------------------------------------------------------

最初の2行はrubyからslang拡張を使うためのおまじない。
画面制御ライブラリを使うためには、まず slang_init_terminal(1, 1)
を呼びます。とりあえず引数の意味は無視しましょう。そしてプログラムを終了
する前に必ず sl_reset を呼びます。でないと抜けたあと画面が変になります。

slsmg_cls で画面を全部消します。slsmg_gotorc で画面中央に行きます。
slsmg_gotorcの第一引数が行数、第二が桁。
sltt_screen_rows と sltt_screen_cols はシステム変数で、rxvt等の
縦と横の文字数です。これはrxvtを縮小拡大すれば変化します。

なお、rubyのメソッド(関数みたいなもん)の引数は括弧でくくらなくても
いいのですが、上のslsmg_gotorcでは直後に括弧が来るんで全体を括弧で
くくらないと文法エラーになります。

slsmg_set_color で色を指定して slsmg_write_string で文字列描きます。
SL_BG_WHITE や SL_RED はシステム定数で、バックグラウンドやフォアグラウンド
の色を指定するのの使います。
いくらこれらで描いても、最後に slsmg_refresh が呼ばれるまで表示
しません。いちいち描画してたら画面がチカチカするでしょう。

slkp_getkey は下級のキー入力で、何かキーが押されるまで待ちます。
もう少し上等な slang_getkey というのもありますけど。

# 初回はこんなところかな

-- 
人生を背負い投げ

菊谷 誠(Kikutani Makoto)  kikutani / sprintmail.com


Date: Sat, 27 Feb 99 09:11:48 +0900
Subject: [s-lang:00703] Re: ruby で学ぶ S-Lang 
 画面制御 
To: s-lang / juice.or.jp (s-lang ML)

金曜の夜なんか仕事する気にならぬので続けてしまおう。

[2] Pagerクラス

いきなりslang拡張モジュールの核心、Pagerクラスっす。

slangアプリのslrnとかmuttとか眺めてると「基本はページャ」
であることに気付きます。slrnで言えば、記事本体を読むページャ、
あるNewsgroupのサブジェクトを見るページャ、Newsgroup全体のページャ
という具合。ヘルプ画面もページャでしょ?

そこで汎用的なページャ用クラスを作りました。
典型的な使用例がslanglibのソースにサンプルとしてpager.rbが入ってます。
短いので貼って解説してしまおう。

----------------------------------------------------------------------
#!/usr/local/bin/ruby
require "kconv"
require "slang"
include Slang

if ENV['LANG'] =~ /sjis/i # 文字コードに関係なく読めるように
  $lang = Kconv::SJIS
else
  $lang = Kconv::EUC
end

class Pager2 < Pager # ページャクラスのサブクラスを作る
  def build_lines # newで呼ばれるメソッド
    if $file == nil  
      fp = $stdin;
    else 
      begin
	fp = open($file, "r")
      rescue
	printf($stderr, "Unable to read %s\n", $file)
	exit
      end
    end

    while fp.gets
      str = Kconv.kconv($_, $lang, Kconv::AUTO)
      add_line(str) # Pagerクラスで定義されている。一行加える
    end
    
    init_lines # Pagerクラスで定義されている
  end

 def draw_bottom # 画面最下部のステータス行に何を表示するか
   slsmg_gotorc(@rmax, @cmin)
   reverse_color
   if $file
     slsmg_printf("%s ", $file)
   else
     slsmg_printf("<stdin> ")
   end
   slsmg_erase_eol
 end

end

# --------- main 

if ARGV.length == 1   # 引数があればファイルとして読み
  $file = ARGV.shift  # なければ標準入力から
else
  if ARGV.length != 0 || $stdin.tty?
    printf($stderr, "Usage: %s [FILENAME]\n", $0);
    exit
  end
  $file = nil
end

slang_mouse_on # マウスを使おう
exit unless slang_init_terminal(1, 1)

# クラスにインスタンス(実体化)
p = Pager2.new(0, sltt_screen_rows-1, 0, sltt_screen_cols-1)

# キーやマウスのキーシーケンスの定義
slkp_define_keysym("\033>", PAGER_EOB)
slkp_define_keysym("\033<", PAGER_BOB)
slkp_define_keysym("q", PAGER_QUIT)
slkp_define_keysym("b", PAGER_PPAGE)
slkp_define_keysym(" ", PAGER_NPAGE)
slkp_define_keysym("\033[M\040", SL_MOUSE_B0)
slkp_define_keysym("\033[M\041", SL_MOUSE_B1)
slkp_define_keysym("\033[M\042", SL_MOUSE_B2)
# インスタンスのメインを回す
p.main_loop
----------------------------------------------------------------------
今見直すと $fileなどというグローバル変数で渡してるのは
ダサダサだけど、昔のコーディングということで勘弁。

このくらいの短かいので、一応まともなページャになるのが面白いでしょ。
マウスを使っているのは蛇足かも。ステータス行をマウスの
左クリックか右クリックするとスクロールするのです。
左右カーソルで水平スクロールもできますよ。

しかし、上のrubyソースを眺めただけでは、なぜ動作するか
さっぱりわからないでしょう。スーパークラスたるPagerクラスの
中身がわからないので当然です。

では次回からそれを説明します。

-- 
人生を背負い投げ

菊谷 誠(Kikutani Makoto)  kikutani / sprintmail.com


Date: Thu, 4 Mar 99 08:53:42 +0900
Subject: [s-lang:00704] Re: ruby で学ぶ S-Lang 
 画面制御 
To: s-lang / juice.or.jp (s-lang ML)

Sat, Feb 27, 1999 at 09:11:48AM +0900 において
Kikutani, Makoto 曰く:

> しかし、上のrubyソースを眺めただけでは、なぜ動作するか
> さっぱりわからないでしょう。スーパークラスたるPagerクラスの
> 中身がわからないので当然です。
> 
> では次回からそれを説明します。

間が空いてしまった。

普通にslanglibを入れてrubyをmake install すると
/usr/local/lib/ruby/slang.rb などができてますが、
ここにPagerクラスの定義があります。

まずはメインループ。

    def main_loop
      @do_break = FALSE
      while TRUE
	check_winch
	update_display
	key_actions_call(slkp_getkey)
	break if @do_break
      end
    end

インスタンス変数の@do_breakがTRUEになるまで無限ループします。
それは key_actions_call のどっかでTRUEになるでしょう(q押されたとか)。
普通は key_actions_call というのは slkp_getkey で受けとったキー入力により
画面をスクロールしたりします。
check_winchはrxvtのサイズが変更されたかチェック
(ちょっとこの実装はまだ貧弱)。

update_display が画面表示の中心で以下のメソッドです。

    def update_display
      slsig_block_signals # CTL-Cとか受けつけないように
      
      normal_color
      
      update_region(@rmin, @rmax, @cmin, @cmax, @col_start, -1)
      draw_bottom
      if @with_mini_buffer
        slsmg_gotorc(sltt_screen_rows-1, 0) 
	slsmg_erase_eol
      end
      slsmg_gotorc(@rmin, @cmin)

      slsmg_refresh
      slsig_unblock_signals # CTL-Cとか受けつけるように
    end

ホンモノではカーソル移動がある場合とで分けてるけどここでは省略。
上でやってるのは、指定された領域をupdate_regionで描画した
のちdraw_bottomでステータス行を書く、ミニバッファ付きならそこ
(画面最下部)をクリアしとくと。

指定された領域というのはPagerクラス(かそのサブクラス)
のインスタンスを作るとき、x = Pager.new(x0, y0, x1, y1) のように
指定します。update_region はCで書かれたルーチンを呼ぶだけ

    def update_region(b, e, cb, ce, off, curline)
      return if e <= b || ce <= cb
      @scrw.update_region(self, b, e, cb, ce, off, curline)
    end # update_region

あー、だんだん説明が困難になってきた。その実体はこれです。

rubyの拡張モジュールの講座ではないので詳しく説明できませんので
わからないところは無視しましょう。

static VALUE
scrW_update_region(obj, self, rbegin, rend, cbegin, 
		   cend, offset, cursor_line)
    VALUE obj;
    VALUE self;
    VALUE rbegin;
    VALUE rend;
    VALUE cbegin;
    VALUE cend;
    VALUE offset;
    VALUE cursor_line;
{
   int row;
   File_Line_Type *line;
   scrWdata *scrWp;
   int b = NUM2INT(rbegin); // 領域の行のはじまり
   int e = NUM2INT(rend);   //           おわり
   int cb = NUM2INT(cbegin); // 領域の列のはじまり
   int ce = NUM2INT(cend);   //           おわり
   int off = NUM2INT(offset); // 水平スクロールのときのオフセット
   int curline = NUM2INT(cursor_line); // カーソル行
   Data_Get_Struct(obj, scrWdata, scrWp);

   if (e <= b || ce <= cb) return FALSE;
   scrWp->Line_Window->nrows = e - b + 1;
   if (scrWp->Line_Window->top_window_line != NULL) {
      scrWp->Line_Window->current_line = scrWp->Line_Window->top_window_line;
      SLscroll_find_line_num (scrWp->Line_Window);
   }
   
   SLscroll_find_top (scrWp->Line_Window);

   row = b;
   line = (File_Line_Type *) scrWp->Line_Window->top_window_line;
   // キモはここからっすよ   
   while (row < e) { // b から e までの行をアップデートします
      SLsmg_gotorc (row, cb);
   
      // カーソル移動モードのときカーソルのある行に色をつける
      if (SLtt_Use_Ansi_Colors) {
	 if (row == curline)
	   SLsmg_set_color(scrWp->cursor_line_color);
	 else
	   SLsmg_set_color(scrWp->normal_color);
      }
      else {
	 if (row == curline)
	   SLsmg_normal_video ();
	 else
	   SLsmg_reverse_video ();
      }

      if (line == NULL) { // ドキュメントの最後で空の部分にチルダを
	 if (scrWp->tilde != 0)
	   SLsmg_write_char ('~');
      }
      else { // 実際に一行を書く
	 if (rb_respond_to(self, rb_intern("write_line"))) {
	    VALUE argv[2];
	    argv[0] = line->data;
	    argv[1] = (row == curline) ? TRUE : FALSE;
	    rb_funcall2(self, rb_intern("write_line"), 2, argv);
	 }
	 else SLsmg_write_char ('?');

	 line = line->next;
      }
      erase_eol(ce, scrWp->bar);


      row++;
   }
   return TRUE;
}

rb_funcall2 というのが苦心のあとでしてね、単に一行書くだけなら
SLsmg_write_string かなんか呼べば済む話。しかしそれでは面白くない。
ここで、ruby側で定義したwrite_lineというのを呼んでるのです。
そうすると、たとえば製作中のメイラでは

  def write_line(l, cursor)
    line = l.data
    .....
    elsif line =~ $quote_pattern  # quote
      slsmg_set_color($quote_color)
    .....
    sl_write_string_with_offset(line, @col_start, @cmax)
    slsmg_erase_eol
  end

などとなって、引用された部分の行だけ色を変えられるわけです。

C側で line->data の構造体データを rubyでは l.data のように
受けるのですが、ここのデータは単なる文字列ではなく、rubyの
インスタンスだったりもするので、一行の中でフィールド別に色を
変えたりすることもできます。この例はslang.rbのTree_Pagerという、
Pagerクラスのサブクラスに例があります。


はー疲れた。続く(かもしれない)。

-- 
人生を背負い投げ

菊谷 誠(Kikutani Makoto)  kikutani / sprintmail.com