3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

rubyでコマンドプロンプトのカーソル移動

Last updated at Posted at 2016-04-02

経緯

さっきポストした記事にも書きましたが、コマンドプロンプトのカーソル移動ってどうやるんだろう?という技術的興味から。

実際の動き

こんな感じで動きます。

aaa.gif

スクリプト

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のエイリアスで対応できたのか不明なので、調査

参考資料

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?