Terminal Text Animation
t6o_o6t.icon
情報技術概論の最終課題
オリジナリティ??
既にPowerPointで教員が挙げている課題に取り組んでもオリジナリティに欠ける部分があるかもしれない
この路線の場合、見せ方を巧くする方向で考え始めるとよいかも
日ごろからやりたいと思っていたことをやる場にするt6o_o6t.icon
Webアプリ上で作成した画面(以下, Image)をターミナルに映すことのできる仕組みを作りたい
作成したImageは設定ファイルに出力できるものとする
どのようにデモを発表するのかという点で、これが最低限である
実用化するためには、以下が課題となる
作成したImageをどのように外部アプリケーションから利用するのか?
Webブラウザを介し、高度なTUI画面を作成することが、このプロダクトに要求される機能である
現状では、以下のアーキテクチャを考えている
作成したImageのrunner(以下,Image Runner)
コンストラクタ引数に、Imageファイル which you want to runと、ttyのデバイスファイルを受け取る。
Image Runnerはshared object fileとしてビルドする
これにより、Node.jsのffiやPythonのctypesからrunnerを、低コストで呼び出せる
現実的には、ラッパー部分まで提供できなければならないだろう
Image Runnerは、内部でgRPCサーバーを起動する
Image RunnerのViewはstatelessであり、外部からの状態の供給が必要
メインプロセスはgRPCサーバに対してリクエストを送ることで、画面に状態を注入する
gRPCサーバはクライアントに対し、ターミナル上で発生した入力イベント等を送信する
gRPCである必要があるのかは不明
もし低コストで型の決まった値を送りあえる方法がほかにあるなら、それでかまわない
gRPCサーバに注入する状態のイメージは、以下のようなもの
code:state_example
{
"state": {
"string_input_0": "hogehoge"
}
}
hogehogeという値を、string_input_0というテキストボックスに表示する例
この例ではstring_input_0というIDに、事前にstring型を割り当てているものとする
Image Runnerに似た機能がデモには必要だが、すべて必要とはいえない
外部からの呼び出し機能は、デモには過剰である
インタラクティブな機能も同様に、過剰である
ImageをWeb上で作成しターミナル上に表示する機能は、必要である
2023/12/10 Start
最低限の機能の検討
Image Editor(/editor)
/editor/frontend/src/features
/editor/frontend/src/features/settings
設定タブ
ターミナル幅の入力
ターミナル高さの入力
/editor/frontend/src/features/canvas
キャンバス
テキスト
ピクセル
/editor/frontend/src/features/runner
Preview ボタン
gRPC hook
Image Preview Server /editor/preview_server
Note:
frontendでPreviewボタンを押すと、gRPC hookからgRPC serverに、Imageが送信される
Image Preview Serverは、Image Runnerに、受信したImageと現在のttyを渡して実行する
授業の制約上Cで実装したい
C++のライブラリがあるので、Preview ServerはC++で書くことにする
C++からCで書かれたImage Runnerのshared object fileをincludeして実行する
C++であれば、このようなことが簡単にできるので良いと思う
gRPC server
runner call
Image Runner/runner
Terminal Drawer /runner/terminal/drawer
これが最低限っぽい
依存関係と逆の方向に作る
今回は、Webアプリ(Image Editor)が依存されている
Webアプリの開発は後回し
Terminal Drawerの実装を最初に行う
Terminal Drawerの実装には、Imageの内容定義が必要になる
Imageの内容を定義する
内部的に利用するものなので、人間可読性は必ずしも必要ない
ピクセルごとの内容をどこに置くのか?
Image Runner側で計算する場合
Image Runnerの実装を誤ると、Image
Image Editorですでにピクセルごとの内容を取得することが容易にできるので、二度手間になる?
インタラクティブ機能を実装するには、Image Runner側で各ピクセルの計算を一度は行う設計にしなければならないのでは?
∵ Image EditorはstaticなImage定義を行うものであり、再生時画面の計算は行わない
⇒ たとえ実装に不安があっても、Image Runner側で計算を行う必要がある
Rendererの処理の一部をWebAssemblyから呼べば、Image Runnerに実装を寄せられそう
Image Editor側で事前計算する場合
Image Editorでは、staticなImage定義しか扱えない
Image Runnerの計算量は減る
Image EditorからImage RunnerへのImageの通信量が過剰になる?
1ピクセル1バイトで表現できたとしても、プレビューボタンを押下するごとに6KiB程度送出される
cf. Terminal Text Animation#65748a5b845875000000c163
現代ではそこまで問題にならなさそうt6o_o6t.icon
スマートフォンではいつもKB単位の通信が裏で行われている
gRPCを使用するかが問題となる
gRPCではどのようにデータを取り扱うのだろうか?
構造化されたデータを取り扱うことは可能か?
要件はgRPCの性質に合致しているだろうか?
Image EditorとImage Runnerとの間で、型を厳密に共有できるのが強み?
REST APIで同じことをすると、OpenAPI -> Swagger -> Aspidaが必要?
今回の要件で、双方向通信は一切不要である
単に、ImageEditor(クライアント)からImageRunner(サーバー)にImageを送信できればかまわない
REST APIのサーバーをC言語で書きたくないt6o_o6t.icon
なぜgRPCを検討しはじめたのかを思い出した
∴ gRPCを使用する。
gRPCで表現することを念頭に置いたデータ構造設計が必要となる
必要な要素
characters
circle
rect
(任意の画像)
これはデモには必要ないので作ってはならない.
code:image_structure.proto
message Point {
int32 column = 1;
int32 row = 2;
}
message Circle {
Point center = 1;
int32 rx = 2;
int32 ry = 3;
}
message Rect {
Point top_left = 1;
Point bottom_right = 2;
}
message Characters {
string content = 1;
Point at = 2;
int32 width = 3;
}
message Layer {
oneof layer_content {
Circle circle = 1;
Rect rect = 2;
Characters characters = 3;
}
}
message ImageStructure {
Repeated Layer layers = 1;
}
Image Runnerの効率化
全画面表示のターミナルは文字数が大きいt6o_o6t.icon
$ mode conn
PowerShellで実行
40 × 156 = 6240
かなり多かった
愚直に毎画面書き換えているとパフォーマンスが悪化する?
□ $ 10^4程度回の計算はまったく問題ないので、ひとつ前の画面と比較し、変更があったピクセルだけを書き換えると良い
ユーザー向け
1. Editorでのレイアウトを完了
2. Editorの「」ボタンを押して、migrate用コマンドをコピー
3. migrate用コマンドを実行
バックグラウンドで、現在のImageのAPI用.protoを落としてくる
cprotoを実行して型定義などを更新
4. migrateされた.protoに合わせて、メインプロセスを実装
画面サイズは変えられた方が良い
Editorにおいて、画面外の要素は白く半透明に重ね塗りする
Editorの画面サイズは、画面端を表す矩形を引っ張って変更できる
いろいろな表示を試せるようにする?
上手くいくか?
操作しにくい
インタラクティブ機能:標準入出力(プロセス間通信)で状態を注入することもできるが..
型のあるプロセス間通信はできないだろうか?
そもそもIPC、RPCは必要?
includeしたrunnerのsoを使うなら、直接同一ファイル内で解決できないのか?
code:runner_example.c
#include "runner.h"
void on_input(const char* value, char* input) {
strcpy(input, value);
}
void on_enter() {
}
int main(void) {
// イベントハンドラはどのように渡す?
// イベントハンドラの型は?
Runner *runner = make_runner("../images/image_0.yaml", on_input, on_enter);
char input20;
// proto2yaml などを使って、yaml形式のimageを事前に取得しておく
// stateの設定
runner.set_page(0);
runner.set_state("key", "value");
runner.set_state("visible", false);
runner.set_state("user_input", input);
// stateの設定を反映
runner.draw();
}
真のインタラクティブ性のために
描画するピクセルを外部から供給できるのが、本当のインタラクティブ性を実現するのに必要なのでは?
それだと、単にすべてのピクセルを直接供給すれば良いということになってしまうか..?
この機能は、Low Level APIとして提供できると良い
!
attributesなどを供給するという考え方
Image Editorで生成されるものを「template」と考えることにする
templateに対しattributesを与え、renderするという仕組みにする
templateの内容をどう処理するか?
テンプレートエンジンの領域に帰着できた!
Low Codeのようになってしまい、表現力が下がって競合との差を付けるのが難しいことがこれまでの悩みだった
テンプレートエンジンの機能性を目安にすれば、実用的かつ実装が現実的なラインを見定めることができるかもしれない
code:template_engine.mermaid
flowchart LR
attributes --> TemplateEngine --> Renderer --> Pixels --> Drawer --> terminal
ImageEditor --> template --> TemplateEngine
このように構成することにより、Drawerは単に、ピクセルの集合を受け取ってTerminalに描画する責務だけを負うことができる!
パラダイムの違いの考察
Pixelの構造
BYTE type
CHARACTER = 1
SQUARE = 2
BYTE content
BYTE[3] foreground
BYTE[3] background
Rendererの実装
args
processed_template(attributesの埋め込み、プリプロセスが完了したtemplate)
attributesなどの不確定要素は存在せず、素直に解釈できる状態になっている
returns
pixels(Pixelの二次元配列)
processed_templateは、3次元の値である
∵ レイヤー × 各レイヤーの内容(2次元)
図形や画像などを処理し、Terminalに出力可能(以下,drawable)な2次元データに落とすのがRendererの責務である
ピクセルをどのように表示するのか?
Reference: https://github.com/eliukblau/pixterm
Unicodeのブロック要素を用いて表示している?
Lower half block
Drawerの実装
args
pixels(Pixelの二次元配列)
Drawerの責務は、ピクセル情報を受け取って表示すること
Rendererは、processed_templateからpixelsを生成することが責務
Lower half blockの概念をRendererが知ることは許されない
!
processed_templateのうち、図形や画像の処理は、WebAssemblyに切り出せないか?
レイヤーの合成処理もWebAssemblyに切り出したい
Image Editor上で適切に編集中Imageを表示するには、合成後のピクセルの状態が必要になる
単一の関数としてではなく、複数の水準の関数をいくつか用意したい
⇒ Terminal Text Animation#6575441884587500006d8e71
Core(Renderer)の実装
最終的には、ドキュメントの要素単位で制御できるシステムが必要である
おそらくImageとはHTMLのようなものである
HTMLが読み込まれてDOMが構築されるように、Imageを読み込むと、文書内の要素のモデル(TOMと呼ぶ)が構築されるようにする
親子関係が表現でき、各要素がスタイル属性および各要素特有の属性を持てることが要件となる
Imageの表現はXMLや、HTML、JSXのほうが適切?
再発明にならないか?
JSXの問題点は、スクリプトが文書構造と同一の場所にある点である
スクリプトも含めて一つの状態を表現するものと考えれば自然である
デザインツールでは、多くの場合文書構造しか扱えない
挙動のような具体は定義せず、抽象のみを扱う
状態の注入方法は、gRPCで良いのだろうか?
TOMに対し要素を追加するなどの命令はどのように扱う?
命令を許可するのではなく、宣言的UIにするよう強制する
宣言的UIを表現できるような言語 & Runtimeが必要
ここで、いま考えているものはDrawerとは本質的に無関係である
Runtimeは状態をTemplateに注入するものである
すなわち、Runtimeは、Terminal Text Animation#657536c884587500006d8e23で考えたTemplateEngineと同一存在である
Runtimeは初期状態としてImageを読み込み、TOMを操作する基盤となり、操作結果のXML等をRendererに渡す
Runtimeの要件
Image file(JSX等)をparseし、TOMをメモリ上に構築できる
TOMを扱うスクリプト(以下,TOMScript)をRuntime上で実行することができる
Runtimeは、ユーザー入力イベントをRendererから受け取ることができる
実行結果のTOMをXMLまたはProtocol Buffersにエクスポートし、Rendererに渡すことができる
TOMScriptの実行タイミングはいつ?
参考:JavaScript
1フレームに1回など、実行タイミングが状態に無関係なアプリでは、pure JSを用いることが多い
ユーザー入力による値の変更などのタイミングにのみ実行したい場合、Reactを使うと高効率である
TOMScriptが実行されると、実行結果をRender Serverに通知する
宣言的UIのなかに動作に必要なすべてのものが記述されていると仮定すると
TOMScriptが再実行されるのではない
イベント駆動で関数などが再実行される
このような事態になることは予想できていた
テキストボックスを縦に並べるようなUIを実現する場合、必ず以下の3つが必要になる
文書構造を表現する言語
状態を操作する言語
それらを実行する(動作させる)環境
過剰である
実用的なアプリケーションには、インタラクティブ性が必要である
インタラクティブ性を与えるには、表現力とコンテナが必要である
既存のプログラミング言語を利用できるようにする必要がある
フローチャートシステムなどを組み上げるのは、今回の課題において本質的でないから
本当にコンテナは必要か?
初心に戻って、以下のようにできないのか?
Terminal Text Animation#657526d08458750000cdafc6
この例では、ほぼ単純にrunnerという構造体を使用していた
構造が整理された現在なら、もう少し別の書き方ができるのではないか
ラッパーライブラリなどを書いて、シンプルにできないだろうか
code:runtime_example.c
#include "renderer.h"
#include "runtime.h"
// definition of my runtime
code:runtime_example.js
// run on V8 or deno
import { DefaultRenderer, DefaultRuntime, ReadImage } from "terminal";
const runtime = DefaultRuntime();
const textbox = runtime.getElementById("textbox");
textbox.addEventListener("change", () => {
});
function runtime() {
}
runtime();
これを純粋に実現しようとするのが誤りか?
GitHub - fathyb/carbonyl: Chromium running inside your terminal
Webブラウザを内部的に使用することで、Web資産をそのまま活用できるのではないか?
関係のない疑問点 -
仮にWebアプリとしてターミナルアプリを実現する場合、メインのプロセスとはどのように連携させるべきだろうか?
画面に戻り値を設ける
画面ごとにImageのようなものを作る
const user_input = start_image("<IMAGE PATH>")という形で画面を表示する
code:event_to_flow.js
grpc.on("session_start", (user) => {
user.textbox1 = "";
user.textbox2 = "";
setSessionUser(user);
});
const end = new Promise((resolve, reject) => {
grpc.on("request", (type, user, value) => {
if (type == "textbox1") {
user.textbox1 = value;
setSessionUser(user);
} else if (type == "textbox2") {
user.textbox2 = value;
setSessionUser(user);
} else if (type == "sendbox") {
grpc.kill();
resolve(user);
}
});
});
const user = await end;
console.log(user.textbox1, user.textbox2);
単純構成にする
型を効かせやすく、非イベント駆動で組み込みやすく、複雑なロジックはあえて書けないようにする
Terminal Text Animation#6575b3d48458750000f16690のように画面ごとに戻り値を設ける
引数を画面に与えることで、テンプレートに値を注入できるようにする
⇒ jinja2などのテンプレートエンジンを用いてもよい
⇒ 値の注入はどのようにする?
Editorでまず型定義を行い、テンプレート中の要素をテキストなどに使用できるようにする
実値の注入は、start_xxx関数に値を渡すことで実現する
注入する値を表すための構造体は必須となる
これは、grpcのprotocで型定義を生成可能
関数で行うのか?
grpcで注入する構造体を定義するので、grpcを利用するほかない
grpcを利用することで、言語依存である関数を使わずに済むので、様々な言語から利用できる
code:inject_value.mermaid
flowchart LR
grpc_client"grpc client"
grpc_server"grpc server"
template_engine"template engine"
MainProcess --> grpc_client --> grpc_server --> template_engine --> Renderer
template --> template_engine
今後の方針
Terminal Text Animation#6575ba438458750000f166ccを原則とする
考え方に迷ったらここに戻る
実装の順番
これまでどおり、Drawerが最優先である
1. Drawer(Pixels |-> Terminal)
2. Renderer(processed_template |-> Pixels)
3. Template Engine
機能
parse template(XML)
grpc server
スタブ
特になし
4. gRPC Wrapper
gRPC client
5. Main Process Example
6. Template Editor