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 には色々な制御が可能で、
- 文字色を変更する
- 背景色を変更する
- 太字・斜体・下線を設定する
- ターミナルのテキストをクリアする (
cls
やreset
相当) - カーソル位置を動かす
といったことが出来るのだ。
全てを網羅するのは困難なこと、ターミナルアプリが対応していない CSI もあることから、今回は「文字色変更」「ターミナルのクリア」「カーソル位置の移動」までにしておいた。
実装に際しては以下を参考にした。
- 対応制御シーケンス
- 対話型コンソールアプリ制作初級者に教えたい「CSI」 - Qiita
- colors in node js console Code Example
- bash:tip colors and formatting - FLOZz' MISC
- Shell script to test support of ANSI color and style codes · GitHub
- ANSIエスケープコード - コンソール制御 - 碧色工房
- ANSI escape code - Wikipedia
- Ubuntu Manpage: console codes - Linux コンソールのエスケープシーケンスとコントロール シーケンス
- console codes(4) - Linux manual page
毎回これらをハードコーディングするのは辛いので、変数化しておいた。一例を以下のような感じ。実装は 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 も覚えたし、ターミナル向けのサーバとして面白いモノが作れそうだ。