経緯
さっきポストした記事にも書きましたが、コマンドプロンプトのカーソル移動ってどうやるんだろう?という技術的興味から。
実際の動き
こんな感じで動きます。
スクリプト
require 'readline'
require 'pp'
require 'Win32API'
module RbReadline
@GetStdHandle = Win32API.new('kernel32', 'GetStdHandle',['L'],'L')
@hConsoleHandle = @GetStdHandle.Call(STD_OUTPUT_HANDLE)
@GetConsoleScreenBufferInfo = Win32API.new('kernel32', 'GetConsoleScreenBufferInfo',%w(L P),'L')
@ReadConsoleOutputCharacter = Win32API.new('kernel32', 'ReadConsoleOutputCharacter',%w(L P L L P),'I')
alias :old_rl_get_char :rl_get_char
def rl_get_char(*a)
c = old_rl_get_char(*a)
my_event_hook
return c
end
def self.my_event_hook
if @prev_end.to_i != @rl_end
x,y = xy
line_end = @prev_end.to_i > @rl_end ? @prev_end : @rl_end
print "\e[" + (y+3).to_s + ";0H" + @rl_line_buffer[0...line_end]
print "\e[" + (y+1).to_s + ";" + (x+1).to_s + "H"
@prev_end = @rl_end
end
end
def self.xy
csbi = 0.chr * 24
@GetConsoleScreenBufferInfo.Call(@hConsoleHandle,csbi)
x = csbi[4,2].unpack('s*').first
y = csbi[6,4].unpack('s*').first
[x, y]
end
module_function :old_rl_get_char, :rl_get_char
end
print "\e[2J" # Clear the screen
print "\e[0;0H" # Move to Top line on the screen
loop do
line = Readline.readline('> ')
break if line.nil? || line == 'exit'
Readline::HISTORY.push(line)
puts "You typed: #{line}"
print "\e[0;0H" # Move to Top line on the screen
print "\e[2K" # Delete current line
end
やったこととアルゴリズム的なもの
入力された文字をリアルタイムで入力行の2行下に表示する。一行したは改行したときにタイプした文字を表示するようにリザーブしておきます。
文字が入力されたこと知るために、rb-readlineを使いました。RbReadlineモジュールの中にrb_event_hookという特異メソッドがあり、これに任意のメソッド名をシンボルで渡してやることで、RbReadlineがキー入力チェックの毎スキャンごとに任意のメソッドを呼ぶようになります。このフックは引数を渡しません。その代り、RbReadlineをレシーバとしてメソッドを呼び出すので、RbReadlineに定義されているインスタンス変数を参照することができます。@rl_end
には現在の文字数が。@rl_line_buffer
には現在の文字列が格納されています。前回のスキャンと今回のスキャンで入力されている文字数に違いがあれば、その文字を2行下に表示するようにしています。
参考にしたサイトの一つmattnさんのコードを使うことで、カーソル移動にも対応できたので、そちらを採用します。内容は参考資料のURL先の記事を参考にしてください。なんしか、rl_event_hookを使うのをやめ、rl_get_charをのエイリアスで対応しました。以前のコードをご覧になられたい場合は、編集履歴からどうぞ。
2行下を相対的に指定したかったので、RbReadlineにxyというメソッドを生やして、その中でWin32APIのGetConsoleScreenBufferInfoを呼び出すことで、現在のカーソル位置を取得し、そこから行数を+2することで2行下に表示することを実現しています。
実際に行数を指定するには、エスケープシーケンスを利用します。print("\e[#{x};#{y}H#{text}") と記述することで、(x,y)の位置にtextを表示するという意味になります。ただ、x, y は 最初の行を1として指定しないといけないので、先ほどのGetConsoleScreenBufferInfoで取得した情報をそのまま渡してしまうと、一列左、一行上に表示されてしまうので、x,yともに+1した値を指定しないと思ったところに表示できません。
今後の課題
-
同時押しした時の文字"!"等に対応したい。=> rl_get_charのエイリアスで対応 -
カーソルキーの移動に対応したい。=> rl_get_charのエイリアスで対応 -
なぜ、rl_get_charのエイリアスで対応できたのか不明なので、調査