curl でリクエストするとカラフルなテキストがアニメーションするサーバを作る

parrot.live という謎のサイトを見つけたので、同じことをやってみる。

目次

parrot.live と ascii.live

$ curl http://parrot.live/

parrot.live というサイトにターミナルから curl を使ってアクセスしてみると、アスキーアートで書かれた Parrot がカラフルに点滅して踊り狂う。

似たようなモノで、ascii.live というモノも見つけた。

$ curl http://ascii.live/list
{"frames":["parrot","clock","nyan","forrest"]}

# 前述と同じ Parrot だがカラフルな点滅はしない
$ curl http://ascii.live/parrot
# 時刻が表示される
$ curl http://ascii.live/clock
# nyan cat が表示される
$ curl http://ascii.live/nyan
# ランニングする人が表示される
$ curl http://ascii.live/forrest

実装を見てみた

ターミナルでこんなことが出来るのかー、どうやってるんだー?と思い、実装を調べてみた。

ascii.live は Go 言語製。アスキーアートを複数用意し、どうにか表示を切り替えることでアニメーションさせていると見える。

parrot.live は Node.js 製。コチラの方が読みやすかった。ReadableStream を使っているのと、何やら制御文字を利用しているのが読み取れた。

自分でも実装してみた

コレなら自分でも作れるかもしれない。そう思って作ってみたのが、curl Animation Server

Node.js 製だが、依存パッケージはないのでいきなり $ npm start で起動できる。

$ curl http://localhost:8080/ にアクセスすると、カラフルなテキストが点滅する、簡素なモノだ。

実装の仕方

以下、実装解説。

CSI

今回のカギとなるのは、CSI : Control Sequence Introducer というエスケープシーケンスだ。

平たく言ってしまえば、ターミナルで Ctrl + C を入力するとプログラムを中止できる ETX などが代表的な、「制御文字」の仲間みたいなモノである。

「ターミナルで文字色を変更する」というと、PS1 を編集してプロンプトを加工するのがよく知られているだろう。

\[\033[31m\]\u

で赤文字にしたホスト名を表示するとかいう、アレである。

parrot.live はこの CSI 文字を含む文字列を、一定間隔でレスポンスしてきていた、というのがカラクリだ。

CSI には色々な制御が可能で、

といったことが出来るのだ。

全てを網羅するのは困難なこと、ターミナルアプリが対応していない CSI もあることから、今回は「文字色変更」「ターミナルのクリア」「カーソル位置の移動」までにしておいた。

実装に際しては以下を参考にした。

毎回これらをハードコーディングするのは辛いので、変数化しておいた。一例を以下のような感じ。実装は GitHub リポジトリの index.js を参照。

const resetAllStyle = '\x1b[0m';
const yellowText = '\x1b[33m';

もっとも簡単な例としては、以下のようにコーディングすれば、黄色い文字列がレスポンスできる。

const server = http.createServer((req, res) => {
  res.end(yellowText + 'Hello World' + resetAllStyle);
});

最後にスタイルを全てリセットする CSI を流すことで、閲覧者のターミナルの文字色等を壊さずに戻してあげている。

一定間隔でレスポンス内容を切り替える : ReadableStream

CSI というエスケープシーケンスを使うと、ターミナルをクリアし、任意の文字色を指定したレスポンスができることは分かった。

しかし、res.end() でレスポンスを返してしまうとそこまでで、アニメーションが出来ない。

アニメーションを行うには、ReadableStream というモノを用意する。

const stream = require('stream');

const server = http.createServer((req, res) => {
  const readableStream = new stream.Readable();
  readableStream._read = () => {};  // 空で実装しておかないとエラーになる
  readableStream.pipe(res);
  const timer = streamer(readableStream);  // setInterval を生成している・後述
  
  req.on('close', () => {
    readableStream.destroy();
    clearInterval(timer);
  });
});

stream は Node.js 組み込みのモジュール。そこから stream.Readable を生成している。_read は適当な空の関数にしておき、レスポンスオブジェクトを pipe() で渡しておく。

ココまで出来たら、あとはアニメーションしたい内容を setInterval() を使って定期的に流してやる。ReadableStream の push() に定期的にテキストを流してやることで、随時レスポンスできる。

const streamer = (readableStream) => {
  // 画面を全クリアし、カーソル位置を戻す CSI
  const clear   = csi.ed.wholeDisplay + csi.ed.wholeDisplayWithScrollBack + csi.cu.cup;
  // カラフルに色付けするための色定義
  const colours = [csi.fg.red, csi.fg.yellow, csi.fg.green, csi.fg.cyan, csi.fg.blue, csi.fg.magenta, csi.fg.white];
  // 表示するテキスト。ココに複数行のテキストを用意し、配列にすれば、AA がアニメーションするような動きにできる
  const text    = 'COLOURFUL TEXT';
  
  let index = 0;
  return setInterval(() => {
    const line = clear + colours[index] + text + csi.reset.all + '\n';  // 末尾にリセットを組み込んでおく
    readableStream.push(line);  // テキストをレスポンスする
    index = (index + 1) % colours.length;  // 配列 colours をループするためのイディオム
  }, 100);  // 100ms 間隔で繰り返し実行する
};

閲覧者は、Ctrl + C で curl を終了させることになるが、その時の切断処理をキレイに行うために、req.on('close') イベントを実装してある。ReadableStream を破棄したり、clearInterval() を呼んだりしている。

最後に、User Agent をチェックして、curl でアクセスしていない人に対しては「curl でアクセスしてね」的なレスポンスを返すよう調整しておくと親切だ。

以上

やってみれば意外とお手軽にできた。ReadableStream も覚えたし、ターミナル向けのサーバとして面白いモノが作れそうだ。