Terminal curses
Terminalの基礎とRuby、そしてcursesについて - Part2

Terminal curses #2/2

RubyKaigi 2019
に開催

2019年4月18日から20日にかけて、福岡国際会議場にて「RubyKaigi 2019」が開催されました。2006年から毎年開催され、今回で13回目を迎えるRubyKaigi。世界中からRubyコミッターを始めとした第一人者が集い、最新の情報や知見を共有します。プレゼンテーション「Terminal curses」に登場したのは、Shugo Maeda氏。講演資料はこちら

スピーカー

curses

Shugo Maeda氏:これからようやくcursesのお話ですけど、端末制御は今まで説明したようにいろいろなことを考えないといけないので、全部自分で実装すると面倒くさいんですが、基本的な端末制御の機能を提供する「curses」というライブラリがあります。

この名前の由来は「cursor optimization」をもじったものだということなんですけど、どうもじったら「curses」になるのかは僕の英語力ではよくわからなくて。Aaronさんに聞いても「これはあんまりおもしろくない名前だね」みたいに言っていたので、ネイティブの人にもちょっとピンと来ないのかもしれないですね。

「curses」というのがオリジナルのLinuxにあったんですけど、最近みなさんがLinuxとかで使うのは「ncurses」だとか、「ncursesw」というwide characterをサポートしたバージョンを使われることが多いと思います。あと「PDCurses」というのは、パブリックドメインのcursesライブラリがあるんですけれども、これがWindowsもサポートしてるので、Windows環境だと主にこちらを利用することになるかなと思います。

Rubyだと、もともとは標準ライブラリで僕が使ったcursesという拡張ライブラリがあったんですけれども、僕がたぶん20年以上前に作ったんですけど、自分でもぜんぜん使ってなくてですね。2.1ぐらいの時に、当時「標準ライブラリからTkを削除したい」みたいな話があって、僕がこれcursesを削除してTkを道連れにしようと思って削除したんですけど(笑)。そういったことがあって今はgemになっています。僕とEric Hodelで主にメンテナンスをしています。

cursesを使ったアプリケーションとしては、僕が作ったTextbringerとか、あと松田明さんが作ったrfd。昔、MS-DOSでFDというファイラがあったそうなんですが、その現代版みたいなものです。ファイルを表示して選択して開いたりできるようなもの。ruby_terminal_gamesというのはゲームをいくつか収めたgemで、Twitter Clientなんかも作っている人がいるみたいです。

Hello worldがこんな感じです。init_screenというのは最初のおまじないみたいなもので、setpos(4, 10)ってやると、4行目の10カラム目にカーソルを移動して、「Hello, world」という文字列を表示します。次に 5行目の10列目に移動して、Press & Enterと出力して、get_charで1文字読み込んで、最後にスクリーンをクローズするという流れです。

Terminalのmodeもサポートしています。「raw」というのが先ほどのio/consoleのrawに似てるんですけど、若干違うのが、echo backの無効化とか、あとCR/LFの変換は無効化されません。先ほどのio/consoleだと無効化されるんですが、そういう制御は別になっています。

あと、「cbreak」というのが「raw」modeとデフォルトのmodeの中間みたいなmodeで、1文字1文字を読み込むんですけど、^Cみたいなキーはシグナルの生成のデフォルトの挙動のままになるというようなmodeです。

「raw」modeなどを使うとコントロールキャラクタの動作が行われなくなって、文字そのものがプログラムから読めるようになっちゃうので、例えば^Zとかをすると、普通はコンソールアプリケーションなどの場合、こんなふうにsuspendして、「fg」って打つとまた戻るという機能があるんですけど、こういうのが自分で実装しないといけなくなるので、ちょっと注意が必要です。

たぶんさっきの機能だと、SIGCONTというシグナルで戻ってくるので、そこでスクリーンの描画をし直すというような実装が必要だったりとか。あと、suspendするときは、例えば「文字を読み込んで^Zだったら、STOPというシグナルを自分に送る」とか、そういう制御を行う必要があります。

そのほかの設定としては、noechoと呼ぶとecho backが行われなくなったりとか、nonlを呼ぶとCR/LFの変換が行われなくなります。最後のkeypad(true)というのは、keypadを有効にすると、例えばファンクションキーなどを押したときに、ファンクションキーというのは対応する文字がないので端末のアプリケーションによっては困ってしまうんですが、そういう特殊なキーもエスケープシーケンスとして返してくれて、アプリケーション側で処理ができます。

文字の入力について

文字の入力については、昔はget_chというメソッドを用意していたんですが、そちらがwide characterをサポートしてなかったので、新しくget_charというメソッドを用意しています。

これを呼ぶと、基本的にはバイトではなくて文字単位で入力が取れるようになっています。例えばさっき説明したファンクションキーとかは対応する文字がないので、数値で返すようにしてしまっています。入力によって型が変わるのであまりきれいな設計ではありませんが、Rubyだとわりといい加減にこういうことをやってしまいがちですね(笑)。

「410」というのが、これはファンクションキーよりももうちょっと特殊で、terminal emulatorとかでウィンドウのサイズを変えると、アプリケーション側でそれを検知して描画し直したりしたいんですけれども、そういうときにシグナルが発生するんですけど、cursesだとシグナルを特殊な文字に変換するような機能がありますので、それを使うとKEY_RESIZEという特殊なキーコードが返ってきます。

あと最後に、EOFのときはnilが返るようになっています。

あと、ちょっと特殊な考慮が必要なのが、Alt keyとかを使って、例えばAlt-aみたいな入力があったときに、どういうふうにプログラム側に渡るかなんですけれども、たいていのターミナルだと、Rubyの文字列リテラルだと、”\M-a”とかやると、そのaという文字の8ビット目を立てた文字を返すようにしてるんですけれども、そっちではなくて、だいたいエスケープのあとにaが来るような設定のターミナルが多いかなと思います。

あと、端末のプログラムで考慮しないといけないというか、考慮してもどうにもならないんですけど、例えば、Shift単体で押してもそのイベントが取れなかったりしますし、あとは、ほかの文字と組み合わせて、例えば^%とか押しても、そういうのは処理できないので。

コントロールキャラクタは全部の文字に対して割り当てられているわけではないので、terminal emulatorによっては、設定をするとそういうのもエスケープシーケンスを自分で定義すれば取れるんですけれども、若干そのあたりがGUIに比べて使いづらいところです。

先ほどのget_charは基本的blocking readなんですけれども、nodelayというフラグをtrueにしてやると、入力がまだなかったときには待たずにすぐ返ってきて、nilが返ってくるというような動作ができます。これが必要なのは、例えばキー入力以外のほかの処理も待たないといけないようなときに、nodelayとか、場合によってはtimeoutを設定して、キー入力がなかったらほかのイベントを調べてなにかするというような処理をしたりします。

ただ、ちょっとこれが使いにくいところは、EOFと区別ができません。cursesのCのライブラリ自体のインターフェースとして、入力がまだ来ていないときとEOFのときは同じコードが返ってくるので、ちょっとここは注意が必要かなと思います。

Windowと画面の再描画

cursesはWindowというクラスを用意していまして、画面全体ではなくて、画面の中の一部分に対して文字を出力することができるようになっています。

画面の再描画なんですけれども、Windowに対してrefreshというメソッドがあって、これを呼ぶとそのWindowがすぐその時点で実際の端末の画面に反映されるんですけれども、だいたいWindowを複数使ったりするともうちょっと効率的に画面の再描画をしたいことが多いので、noutrefreshというメソッドを呼ぶと、cursesの中で仮想的な画面データを持っているんですが、そちらを先に更新して、最後にCurses.doupdateとやると実際にそれが端末に反映されるという機能があります。

cursesを使っていて一番うれしいのがこの機能で、端末ごとに使えるエスケープシーケンスとかが違って、だいたいTUIのアプリケーションって画面全体を書き換えるだけではなくて一部分だけ変わることが多いんですけれども、そういう場合に各端末に応じて一番効率的な一部分の編集だけをしてくれるという機能です。

あとは、端末のプログラムで面倒くさいのが文字の幅を考えないといけないことです。その文字が端末上で何列分の表示幅を必要とするかを考えなければいけません。

ITOYANAGIさんの発表で紹介されていた方法として、実際にその端末に文字を書いてカーソルのポジションを得るという方法があります。ですが、効率が少し悪いのと、cursesの場合は実際の画面のカーソルポジションじゃなくて、基本的にはcursesのライブラリが持っている仮想的な画面のカーソルポジションを扱うので、これはTextbringerでは使っていませんです。

Cの関数ではwcwidthというwide characterの表示幅を取る関数がありますので、これが使われることが一般的です。cursesもncurseswというwide characterをサポートしたバージョンだとwcwidthのほうを使うようになっています。

RubyではUnicode::DisplayWidthというgemがありまして、Unicodeに限定されますが、ある文字の表示幅を得ることができます。

ただ、ちょっと考慮が必要なのが、East Asian Ambiguous Widthです。一部の記号はコンテキストによって表示幅が違うことがありまして、日本語環境だとだいたい2桁分になりますが、このように各レイヤーでその文字幅を同じように扱わないと、文字が重なって表示されたりカーソルの位置がおかしくなったりという不具合が発生します。

あと、wcwidthを主に日本語とか中国語・韓国語の環境で使うときは、例えばLD_PRELOADを使って、さっきの曖昧な文字幅を2を返すように修正するとか、localeのデータでcharmapを書き換えてそういった対応を行うという考慮が必要になります。

Windowsにおけるcurses

ここからWindowsの話をしたいと思います。新しいWindows Consoleでcursesを使うと、今は直したんですが、このように文字が重なって表示されるバグがありまして。

Windows Consoleって、先ほどターミナルはファイルI/Oでという話をしましたが、Windowsはぜんぜん独自の専用のAPIを用意しています。

High-levelなAPIだと「ReadConsole()」「WriteConsole()」、Low-levelなAPIだと「ReadConsoleInput() 」「WriteConsoleInput()」「ReadConsoleOutput()」「WriteConsoleOutput()」。

InputなのにReadとWriteがあるのは、入力のバッファを書き換えることができたりとか。Outputのほうは、実際に今表示されている画面全体を読んだり全体を書いたりできるようになっています。

ただ、これが新しいWindows Consoleだとうまく動いていなかったので、PDCursesだと、効率は悪いですが、1文字ずつカーソルポジションを設定して出力するようにしています。

最近リリースしたcursesでmenuとformのライブラリをサポートしていますが、時間がないのでこのへんのコードは飛ばします。

メリットとしてmenuとかformというHigh-levelなインターフェースが提供されるんですが、1つの問題としてはPDCursesでサポートしていないとのと、このように自分でキー入力を読んでメニューの制御などをを行わないといけないので、イベントドリブンじゃなくて面倒くさいです。

ただ、cursesはフレームワークじゃなくてライブラリなので、これは仕方ないかなと思ってまして。

TUIのフレームワークがあると便利だと思うんですけど、「Textbringer」というテキストエディタを使うとメーラーが書けたりとか、スタートレックのアニメーションが表示できたりとか、だいたいなんでも作れますので、ぜひみなさん使ってみてください。

あと最後、参考文献として、『詳解UNIXプログラミング』という書籍でわりと端末の詳しい内容が書いてありますし、たぶんptyのお話とかは田中哲さんが『APIデザインケーススタディ』という書籍で解説されています。

みなさん、ご清聴ありがとうございました。

(会場拍手)

詳解UNIXプログラミング

APIデザインケーススタディ ~Rubyの実例から学ぶ。問題に即したデザインと普遍の考え方 (WEB+DB PRESS plus)

  • Credit DEC VT100 terminal at the Living Computer Museum https://en.wikipedia.org/wiki/Computer_terminal#/media/File:DEC_VT100_terminal.jpg Jason Scott CC BY 2.0
Occurred on , Published at

RubyKaigi

RubyKaigiに関する記事をまとめています。コミュニティをフォローすることで、RubyKaigiに関する新着記事が公開された際に、通知を受け取ることができます。

このログの連載記事

1 Terminal curses––Terminalの基礎とRuby、そしてcursesについて - Part1
2 Terminal curses––Terminalの基礎とRuby、そしてcursesについて - Part2

スピーカーをフォロー

関連タグ

人気ログ

人気スピーカー

人気コミュニティ

ピックアップ

編集部のオススメ

ログミーTechをフォローして最新情報をチェックしよう!

人気ログ

人気スピーカー

人気コミュニティ

ピックアップ

編集部のオススメ

そのイベント、ログしないなんて
もったいない!
苦労して企画や集客したイベント「その場限り」になっていませんか?