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

Line 1: Error: Invalid Blog('by Esehara' )

または私は如何にして心配するのを止めてバグを愛するようになったか

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

Rubyで、矢印キーにて伸びたり縮んだりするようなバーをコンソール上で実現する

今日の料理

f:id:nisemono_san:20160406111702j:plain

油あげの鍋

概要

CUI(キャラクター・ユーザー・インターフェイス)上で、何かしらの常駐コマンドを作成した場合、キーイベンドによって、何らかの操作が出来たほうが望ましい場合がある。そういったコマンドをRubyで作ろうとした場合、意外にもまとまったドキュメントが存在しなかったため、今回調べた範囲で、そのようなコマンドを作る方法をまとめてみることにする。

どういうこと?

例えば下のようなもの:

f:id:nisemono_san:20160412025207g:plain

素朴にキーを習得する

単純にキーボードの入力を受けとるとするならば、このStackoverflowのようなやり方がある。

Thread.new do
  while c = STDIN.getc
    puts c.chr
  end
end

loop do
  puts Time.new
  sleep 0.7
end

この単純なコードには、一つの示唆が含まれている。それは、キーイベントなどは、別スレッドにして、その中で補足するのが望ましいということである。下のメインループに組みこんだ場合では、sleepの間、キーイベントは補足できなくなる。このように、イベント毎にスレッドにするというのは、一つの方法であるのは間違いない。

しかし、上の方法は欠点がある。普通、キーボードのイベントを補足するとした場合、一つのキーをタイプするごとに、それを受けとりたいと思うだろう。だが、この場合だと、改行するまで、キーは補足されない。

さて、上のStackoverflowでは、変わりにCursesというライブラリを使うことを提案している。

Cursesについて

Cursesは、ここのドキュメントによれば、ウィンドウライクなAPIを提供するものである。とはいえ、このライブラリに関しては、少々複雑な過程がある。そもそも、このライブラリは標準だったのだが、 2.1.0 で gem ライブラリとして切り離されたという記述がある。

さて、このCursesというライブラリなのだが、やはりややこしいことには変わりがない。というのは、あくまでも「CUI上に仮想的なWindowシステムを構築する」ということになっているからだ。そのあたりについては、このエントリに詳しい

で、これでいける!と思ったが、実は日本語だと文字化けする。setlocal使うといいのではみたいな記事があったのだが、面倒臭いので、代価物を探すことにしたのであった。

ncurseswを使う

そこで、ncurseswというライブラリがあることに気がついた。これは、cursesを強めたncursesに、日本語が使えるようにしたものである。Ubuntuで利用する場合には、ここを参考にされたし

簡単な使い方としては、次のようになるだろう:

require `ncursesw`

$use_ncurse = false
def ncurse_initialize
  Ncurses.initscr
  Ncurses.cbreak #キーボードをバッファしない
  Ncurses.noecho #キーボードが入力されたとき、キーボードを表示しない
  Ncurses.nonl   #新しいラインに移動しない
  $use_ncurse = true
end

def ncurses_exit
  Ncurses.echo
  Ncurses.nocbreak
  Ncurses.nl
  Ncurses.endwin()
end

begin
  # ... Do Anything ...
  Ncurses.printw("Hello, Ncurses world.\n")
rescue Interrupt
  if $use_ncurse
    ncurses_exit
  end
  exit 0
end

Ncursesは、割と癖があって、最初と最後の指定がないと、コンソールが無茶苦茶なことになって、一旦リセットを強いられることになる。また、この中で気になることとしては、Ncurses.printwだと思うのだけれども、こいつに関しては、C言語的な意味でのprintfを提供している。

さて、このエントリの目的は、どのようにコンソールでキーイベントを習得するかということであった。先にその部分についてのコードをのせてみることにする。

def loop_ncurse
  while ch = Ncurses.getch
    life = $life.count
    case ch
    when 48
      life = 0
    when 49
      life = 1
    when 50
      life = 2
    when 51
      life = 3
    when 52
      life = 4
    when 68
      life -= 1
    when 67
      life += 1
    end
    save_in_loop life
  end
end

ちょっと汚いコードになっているのはご愛嬌として、要するにNcurses.getchを待ちぶせしていればいいということになる。ちなみに、矢印キーについては、エスケープ文字を含めて三文字程度ある(それについては、次のリンクに任せる)

もっと簡単な方法はないものか

とはいえ、何度も言うように、Ncursesが仮想的なウィンドウを提供するものであるからして、これはちょっとやりすぎである。なので、もうすこし軽量な方法はないものか、ということで探してみると、Qiitaにドンピシャなものがあった。このコードをちょっと改良して、下のように書いてみた。

def key_parse
  key = STDIN.getch
  if key == "\e" && STDIN.getch == "["
    case STDIN.getch
    when "C"
      key = ""
    when "D"
      key = ""
    end
  end
  return key
end

def loop_console_io
  while (key = key_parse) != "\C-c"
    life = $life.count
    case key
    when "0"
      life = 0
    when "1"
      life = 1
    when "2"
      life = 2
    when "3"
      life = 3
    when "4"
      life = 4
    when "5"
      life = 5
    when ""
      life += 1
    when ""
      life -= 1
    end
    save_in_loop life
  end
end

これだと、軽量な形でコンソール上のキーをやりとりできるのである。

蛇足: Thread上のバグについて

Thread.abort_on_exceptionは、デフォルトだとfalseであり、false の場合、あるスレッドで起こった例外は、Thread#join などで検出されない限りそのスレッドだけをなにも警告を出さずに終了させるので、trueにして、エラーを補足できるようにするといいと思う。

まとめ

というわけで、一連の試行錯誤をブログにまとめてみた。確かにCUIでリッチな表示を作るのは楽しい一方で、この程度のやつなら、GUIにするべきだったのかもしれないなと思ったりした。適所適材というものは存在するので、次はそういう観点から、GUIにも挑戦したいと思ったりした。