@ledsun blog

無味の味は佳境に入らざればすなわち知れず

Ractorを使ったTUIプログラムの一歩

前回のユーザー入力とクロックを両方待つプログラムを少し改造します。

  • Enter以外の入力を受け取る
  • カーソルを移動せずに書き換える

後者はRactorは関係ないです。

Enter以外の入力を受け取る

任意の1文字を受け取るためにSTDIN.getchを使いたいです。 STDINは共有不可能オブジェクトです。 次のようなRactorを書くとcan not access non-sharable objects in constant STDIN by non-main Ractors (NameError)が発生します。

# ユーザー入力を待つRactor
input = Ractor.new do
  while  "\C-c" != STDIN.getch
    Ractor.yield :reset
  end
end

共有不可能オブジェクトをRactorに渡すときはmoveします。

# ユーザー入力を待つRactor
input = Ractor.new do
  io = Ractor.recv
  while  "\C-c" != io.getch
    Ractor.yield :reset
  end
end

input.send STDIN, move: true

カーソルを移動せずに書き換える

エスケープシーケンスを使ってカーソルを操作します。

  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val

完成形

require 'io/console'

# ユーザー入力を待つRactor
input = Ractor.new do
  # moveされたSTDIOを使って文字入力を待つ
  io = Ractor.recv
  while "\C-c" != io.getch
    Ractor.yield :reset
  end
end

# クロックイベントを発生するRactor
clock = Ractor.new do
  loop do
    Ractor.yield nil
    sleep 0.3
  end
end

# 共有不可能オブジェクトSTDINをRactorにmoveする
input.send STDIN, move: true

# 初期値
val = 0
loop do
  # ユーザー入力とクロックを両方待ちます。
  _, flag = Ractor.select input, clock

  # イベントがユーザー入力だったら値をリセットします。
  val = 0 if flag == :reset

  # カウントアップします。
  val += 1
  # 行をクリア
  print "\e[2K"
  # 行頭へ移動
  print "\e[0G"
  # 出力
  print val
end

次のように動きます。一定のペースでカウントアップし、何か入力すると1に戻ります。

f:id:ledsun:20200421225620g:plain
プログラム実行時のスクリーンショット

参考