Ruby: ANSIエスケープシーケンスと cowthink

ANSIエスケープシーケンスと cowthink のメモです。(お遊びメモです)

# ANSIエスケープシーケンス
ANSIエスケープシーケンスは、文字色を変えたりできる画面制御用シーケンスコードです。
解説はここでは割愛します。(ネットにはたくさん解説がありますので)

#cowthink
cowthink は アスキーアートを出力するコマンドです。cowsay に含まれます。
僕は、以下の投稿で知りました。
- [コンソールでAA文字](http://qiita.com/yohhoy/items/c7d457479062f5bd3834 "コンソールでAA文字")

(Ubuntu Linux の場合の) 上の投稿で紹介されているコマンドのインストール方法です。

```shell-session
$ sudo apt-get install cowsay      # cowsay (cowthink が含まれるパッケージ)
$ sudo apt-get install sysvbanner  # banner
$ sudo apt-get install figlet      # figlet
$ sudo apt-get install toilet      # toilet
```

で、この cowthink を動かしてみたいなぁ、と前から思っていたのでやってみました。

![cowthink-yoko.gif](https://qiita-image-store.s3.amazonaws.com/0/60624/14be16bf-931a-3132-58e2-a94dbfdea03d.gif)

# yoko-scroll.rb
cowthink を動かしているスクリプトです。

```shell-session
# 使い方の例

$ cowthing Hello | ruby ./yoko-scroll.rb
```

```rb:yoko-scroll.rb
trap(:INT) { exit 0 }

COLS, RAWS = 80, 24
TOP = 1 + 1

lines  = $<.readlines.first(RAWS).map(&:chomp) + ['']
maxlen = lines.map(&:size).max
lines  = lines.map{|s| (' ' * COLS) + s.ljust(maxlen + 1) }

cls  = ->   { "\e[2J" }
move = -> y { "\e[#{y}H" }

interval = -> { sleep 0.1 }

lines.first.size.times do |i|
  print cls.() + move.(TOP)

  puts lines.map {|s| s[i, COLS]} * $/

  interval.()
end
```
(もちろん cowthink 以外でも)標準入力、あるいは引数で指定したファイルの内容を横にスクロールさせながら表示します。
適当な作りで不足したところも多々ありますが、お遊びのものなので、これ以上はメンテしません。
悪しからず。


#cowthink のメッセージに色をつける
上のスクリプトとは関係ありませんが、cowthink のメッセージに緑色をつけようと思いました。
echo に解釈させるのと同じように「\e[32m...\e[m」でくるんでみましたが。。。

```
$ cowthink "\e[32mHello\e[m"       # これでは、うまくいかない。。。。
``` 

うーん、残念ダメでした。
。。。と諦めてしまっては、そこで試合終了です。
以下のようにすると、うまくいきました。「\e」の部分２箇所を変えています。

![cowthink.png](https://qiita-image-store.s3.amazonaws.com/0/60624/24887ee4-0a48-5a6b-d304-f1bb4a1cb0b7.png)

表示上は「^[」となっていますが、実際はこの部分は生の「ESC」を入力しています。
大抵の Unix 系のターミナルでは、生コードの入力は「Ctrl-V」に続けてそのコードを入力します。
(Ctrl-V は、プログラミング言語での、文字列リテラル内で次に続く特殊な文字の意味を打ち消す「\」のようなものと思ってください)

    [Ctrl] + 'V', [ESC]    # [Ctrl]キーと「V」キーを同時に押して、続けて「ESC」キーを押す

「^[」 という表示は、「Ctrl + '['」をあらわしています。
Ctrl は上位3ビットをマスクするので、以下のようになります。

    0x5b (ASCII '[')    0101 1011
    Ctrl 修飾          & 0001 1111     # Ctrl は上位3ビットをマスクする
    ------------------------------
    0x1b (ASCII ESC)    0001 1011

なので、「Ctrl-V, [ESC]」は「Ctrl-V, Ctrl-[」と入力しても同じです。

```
Ctrl-[ : 0x5b (ASCII '[') & 0x1f (Ctrl) => 0x1b (ASCII ESC)
Ctrl-H : 0x48 (ASCII 'H') & 0x1f (Ctrl) => 0x08 (ASCII BS 後退(バックスペース))
Ctrl-I : 0x49 (ASCII 'I') & 0x1f (Ctrl) => 0x09 (ASCII HT 水平タブ)
Ctrl-M : 0x4d (ASCII 'M') & 0x1f (Ctrl) => 0x0d (ASCII CR 復帰(リターン))
```

制御コードが送られて実際にどう処理するかは、ドライバやアプリ次第です。
ちなみに、上の yoko-scroll.rb では、入力に制御コードが含まれるかどうかなどは全然対応していません。悪しからず。

# ansi_escape_sequence.rb
余談。
関連するスクリプトを掲載しておきます。
というか、ディスクの隅にこのスクリプトを見つけたのがこのメモの発端です。
多分、[これ](http://qiita.com/lnznt/items/0e582a867e34436ce126 "Ruby: pry の起動時に色付きバナーを出すスクリプト")を作るときの実験コードだったと思います。

```rb:ansi_escape_sequence.rb
#!/usr/bin/env ruby
# -*- coding: UTF-8 -*-
$KCODE = 'UTF-8' if RUBY_VERSION < '1.9'

module ANSI
  module EscapeSequence
    extend self

    def esc(ctl, *ss)
      "\e[#{ss.join ';'}#{ctl}"
    end

    def move(y, x=1)  ; esc:H, y, x ; end
    def home          ; esc:H       ; end

    def up(y=1)       ; esc:A, y    ; end
    def down(y=1)     ; esc:B, y    ; end
    def right(x=1)    ; esc:C, x    ; end
    def left(x=1)     ; esc:D, x    ; end

    def cls(mode=2)   ; esc:J, mode ; end
    def insert(n=1)   ; esc:L, n    ; end
    def delete(n=1)   ; esc:M, n    ; end
    def del(mode=2)   ; esc:K, mode ; end

    def save          ; esc:s       ; end
    def undo          ; esc:u       ; end

    def cursor_on     ; esc:l, '5>' ; end
    def cursor_off    ; esc:h, '5>' ; end

    def color(s0, *ss)
      seq = ([s0] + ss).map do |s|
        { Reset:      '',
          Bold:       1,
          Underline:  4,
          Reverse:    7,

          Black:      30,
          Red:        31,
          Green:      32,
          Yellow:     33,
          Blue:       34,
          Magenta:    35,
          Cyan:       36,
          White:      37,

          BBlack:     40,
          BRed:       41,
          BGreen:     42,
          BYellow:    43,
          BBlue:      44,
          BMagenta:   45,
          BCyan:      46,
          BWhite:     47,
        }[s] || s
      end

      esc:m, *seq
    end

    def wrap(s, *ss)
      color(*ss) + s + reset
    end

    alias :seq :wrap

    def reset     ; color:Reset     ; end
    def bold      ; color:Bold      ; end
    def underline ; color:Underline ; end
    def reverse   ; color:Reverse   ; end

    def black     ; color:Black     ; end
    def red       ; color:Red       ; end
    def green     ; color:Green     ; end
    def yellow    ; color:Yellow    ; end
    def blue      ; color:Blue      ; end
    def magenta   ; color:Magenta   ; end
    def cyan      ; color:Cyan      ; end
    def while     ; color:White     ; end

    def bblack    ; color:BBlack    ; end
    def bred      ; color:BRed      ; end
    def bgreen    ; color:BGreen    ; end
    def byellow   ; color:BYellow   ; end
    def bblue     ; color:BBlue     ; end
    def bmagenta  ; color:BMagenta  ; end
    def bcyan     ; color:BCyan     ; end
    def bwhile    ; color:BWhite    ; end

  end
end

if __FILE__ == $0
  ESC = ANSI::EscapeSequence

  p ESC.move 1, 3                           #=> "\e[1;3H"
  p ESC.home                                #=> "\e[H"
  p ESC.cls 1                               #=> "\e[1J"
  p ESC.cls                                 #=> "\e[2J"
  p ESC.underline                           #=> "\e[4m"
  p ESC.red                                 #=> "\e[31m"
  p ESC.bcyan                               #=> "\e[46m"
  p ESC.color :Bold, :Green                 #=> "\e[1;32m"
  p ESC.seq "hello", :Underline, :BBlue     #=> "\e[4;44mhello\e[m"
end

# vi:set ts=2 sw=2 et:
```

# おわりに
このメモのスクリプトなどの動作は以下の環境で行っております。

- Ubuntu Linux 14.04
- Ruby 2.1.5

---
エビデンス取るのにはいつも時間がかかりますが、今回アニメーションgif作るのにはスクリプトの数倍の時間がかかりました。。。