Ruby で ANSIエスケープシーケンスを扱う

Ruby を使っているとき、Linux でのターミナル表示の処理の仕方をすぐ忘れてしまうので、簡単なライブラリを作った。


20181212173044
以下はこんな画面を作るコード。

require_relative 'es'

res = ES.reset

print ES.clear
puts ES.color(:style, :blink) + ES.color(:green, :bright) + "blink" + res
puts ES.color(:style, :underline) + ES.color(:blue, :background) + "undlerline"
print ES.down

print res
16.times do |i|
  puts (0...16).map {|j| ES.color(:ex_b, n = 16 * i + j) + "%02X" % n + res + " "}.join
end

print res + ES.push
print ES.cursor_r(5, 2)
print ES.color(:cyan) + ES.color(:style, :reverse) + "cursor move" + ES.pop

凡例。

  • ES.color(:blue) で文字色を青にする。背景は ES.color(:red, :backgroud) という風に指定する。
  • 拡張文字色(256色)は ES.color(:ex_c, n)、拡張背景色は ES.color(:ex_b, n)
  • 文字の点滅、下線、反転、太字は ES.color(:style, :blink), ES.color(:style, :underline), ES.color(:style, :reverse), ES.color(:style, :bold) のように。
  • 指定を解除するには ES.reset とする。

 

  • 画面クリアは ES.clear、行頭へのカーソル移動は ES.top
  • カーソルの一行上、一行下はそれぞれ ES.up, ES.down
  • カーソルの絶対位置の指定は ES.cursor(x, y)(x, y は自然数)、相対的な移動は ES.cursor_r(x, y)(x, y は 0 や負数も許す)。
  • 状態の保存は ES.push、復帰は ES.pop

その他、ここのサイトの記述を取り入れております。

ライブラリのコード

es.rb

require 'io/console'

module ES
  S = "\e["
  Col = %i(black red green yellow blue magenta cyan white ex_c ex_b reset style code)
  Cmd1 = %i(CUU CUD CUF CUB CNL CPL CHA CHT CBT ECH DCH IL DL SU SD REP DSR)
  Do1  = %w(A B C D E F G I Z X P L M S T b n)
  Cmd2 = %i(DECSC DECRC RIS DECALN IND NEL HTS VTS PLD PLU RI DSC SOS ST)
  Do2  = %w(7 8 c #8 D E H J K L M P X /)
  
  def color(col, opt = nil)
    ch = Col.map.with_index {|c, i| [c, i + 30]}.to_h[col]
    case ch
    when 38
      return S + "38;5;#{opt.to_i}m"
    when 39
      return S + "48;5;#{opt.to_i}m"
    when 40
      return S + "39;40m"
    when 41
      n = case opt
          when :bold           then 1
          when :italic         then 3
          when :blink          then 5
          when :reverse        then 7
          when :blink_stop     then 25
          when :underline      then 4
          when :bold_stop      then 22
          when :underline_stop then 24
          else opt
          end
      return S + "#{n}m"
    when 42
      return S + "#{opt}m"
    end
    raise "Undefind color name: #{col}" unless ch
    m = case opt
        when :background then        10
        when :bright     then        60
        when :bright_background then 70
        else 0
        end
    S + (ch + m).to_s + "m"
  end
  
  def csi(*args)
    cm = Cmd1.zip(Do1).to_h
    if (a = cm[args[0]])
      S + args[1].to_s + a
    else
      case args[0]
      when :CUP then S + "#{args[1]};#{args[2]}H"
      when :ED  then S + (args[1] ? args[1].to_s : "") + "J"
      when :EL  then S + (args[1] ? args[1].to_s : "") + "K"
      when :TBC then S + (args[1] ? args[1].to_s : "") + "g"
      when :DECSTBM
        S + (args[1] ? "#{args[1]};#{args[2]}" : "") + "r"
      when :DECTCEM
        S + "?25" + args[1]
      else
        raise "#{args[0]} is undefined CSI."
      end
    end
  end
  
  def cmd(given)
    cm = Cmd2.zip(Do2).to_h[given]
    cm ? "\e" + cm : raise("#{given} is undefined command.")
  end
  
  def esc(str)
    "\e" + str
  end
  
  def clear() ES.csi(:ED, 2) + ES.csi(:CUP, 1, 1) end
  def down()  ES.cmd(:NEL) end
  def up()    ES.cmd(:RI) end
  def reset() S + "0m" end
  def top()   ES.csi(:CHA, 1) end
  def home()  ES.cursor(0, 0) end
  def push()  ES.cmd(:DECSC) end
  def pop()   ES.cmd(:DECRC) end
  
  # カーソルの移動
  def cursor(x, y = nil)
    y ? ES.csi(:CUP, y, x) : ES.csi(:CHA, x)
  end
  
  # 相対的なカーソルの移動
  def cursor_r(x, y)
    st = ""
    st += if x > 0
      ES.csi(:CUF, x)
    elsif x < 0
      ES.csi(:CUB, -x)
    else
      ""
    end
    st += if y > 0
      ES.csi(:CUD, y)
    elsif y < 0
      ES.csi(:CUU, -y)
    else
      ""
    end
  end

  def console_size
    [`tput cols`, `tput lines`].map(&:to_i)
  end
  
  # スクロールする行の範囲を指定する(引数がなければ範囲の解除)
  def scroll(rn = nil)
    return case rn
           when Range then ES.csi(:DECSTBM, rn.first, rn.max)
           when 0     then ES.csi(:DECSTBM)
           else ES.push + ES.csi(:DECSTBM) + ES.pop
           end
  end
  
  def cursor_position
    puts "\x1B[6n"
    res = ""
    STDIN.raw do |io|
      until (c = io.getc) == 'R'
        res << c if c
      end
    end
    m = /(\d+);(\d+)/.match(res)
    [m[2], m[1]].map(&:to_i)
  end
  
  # 下にn行開けてカーソルを始めの位置にもってくる
  # (既に下にn行以上開いていたら何もしない)
  def safe_scroll(n)
    return "" if n <= 0
    str = ""
    y = ES.cursor_position[1]
    if (h = ES.console_size[1]) < y + n
      str = ES.scroll_up(n - 1)
      y = h - n
    end
    str + ES.cursor(1, y)
  end
  
  def scroll_up(n)  "\n" * n end
  def clear_below() ES.csi(:ED) end
  
  module_function :color, :csi, :cmd, :clear, :down, :up, :reset, :top,
    :cursor, :cursor_r, :home, :push, :pop, :esc, :console_size, :scroll,
    :scroll_up, :cursor_position, :clear_below, :safe_scroll
end

 

使用例1

20181219202437

これのコードは以下です。
ANSIエスケープシーケンスで遊ぶ · GitHub
 

使用例2

require_relative 'es'

y = ES.console_size[1]
print ES.safe_scroll(1)
print ES.push
print ES.scroll(1..y - 1) + ES.cursor(1, y) + ES.color(:green, :bright) + "FIXED"
print ES.reset + ES.pop
10.times do |i|
  puts i
  sleep(0.1)
end
print ES.scroll + ES.clear_below + ES.reset

 
※参考
ANSIエスケープコード - コンソール制御 - 碧色工房
VT100のエスケープシーケンス - BK class