Edited at

コンソールでキー入力を一文字づつ読み込む

More than 1 year has passed since last update.

例えば簡単なCLIのゲームやpecoのようなキーボード入力が一回あるたびに何か動作させるものを作りたい場合は、OSの機能に働きかけ、端末の動作を変えることで実現できます。

Rubyでの場合を紹介しますが、

基本的にどのプログラミング言語でもこの端末の機能に触ることができれば実現できるはずです。


ルーク、io/consoleをつかえ

Rubyでは、標準添付ライブラリーio/consoleで使えるようになるIO#rawを使用します。

主要なOSに対応しているのでwindowsでもlinuxでもmacで同じように使えるでしょう。

「ふつうにIO#getcでいいじゃん」と思う方はirbなどでSTDIN.getcを実行してみましょう。

$ irb

irb(main):001:0> STDIN.getc
asdf...

エンターキーを入力するまで文字の読み込みが始まらないことが分かります。

ではrequire 'io/console'してからSTDIN.raw(&:getc)を実行してみましょう。

$ irb

irb(main):001:0> require 'io/console'
=> true
irb(main):002:0> STDIN.raw(&:getc)
=> "a"

一文字入力した途端にirbに戻ってきます。


しくみ

端末には一般的にrawモードcookedモードがあるようです。(他にもnoechoモードなどもあるが省略)

私たちが普段使っているようなものはcookedモードで、


  • CTRL+Cなどのようにシグナルを送れる

  • 一行分バッファリングするので途中でカーソル移動したり消したりできる

  • 端末に入力した文字が表示される

という特徴があります。

逆にrawモードは


  • シグナルを送れない

  • バッファリングしない

  • 端末に入力した文字を表示しない

という特徴があります。

Rubyでもこのモード切り替えが簡単に操作できるようになっていて、



  • IO#raw: ブロックの間だけrawモードにする


  • IO#cooked: ブロックの間だけcookedモードにする

と使い分けることができます。

以降ずっとrawモードにするIO#raw!などもあるのですが、rawモードのままプログラムを終了すると、

コンソールの表示が変な感じになったりします。

ブロック付きで使うと、たとえエラーが起ころうとブロックを抜けるときに自動的に元の状態に戻してくれるのでおすすめです。

以下は入力したキーボードのバイト値を、

キーボードを打つ毎に端末の左上に表示するサンプルです。

#! /usr/bin/env ruby

require 'io/console'

$stdin.raw do |io|
while true
ch = io.readbyte
exit 0 if ch == 3 # CTRL+Cで終了
print "\e[H" # カーソルをコンソールの左上に移動する
print "\e[0K" # 前の文字が残らないようにする
print ch
end
end

このようにエスケープシーケンスなどと組み合わせれば、タイピングゲームやオレオレエディタなんかも作れて夢が広がります。


Cでの実装

基本的に#include <termios.h>でつかえるAPIを利用します。

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/termios.3.html

実践的な実装は https://github.com/ksss/mruby-io-console/blob/master/src/io-console.c あたりをご参考ください。

macでしか動作確認してない & win対応してないですが……。