- Textual の開発過程では、古いターミナルプロトコルの上でも滑らかな TUI アニメーションを作れるが、画面更新方式と出力同期を慎重に扱う必要があることが分かった
- ちらつきを減らすには、画面を消去せずに上書きし、標準出力へ一度に書き込み、Synchronized Output プロトコルを活用する方法が重要
- Python の
dict.items() ビューと @lru_cache は、レンダリング差分の計算や小さな高頻度関数の最適化に有用で、CPython の C 実装がキャッシュ性能を支えている
- ほぼイミュータブルなオブジェクト、Unicode の罫線文字による図、
fractions.Fraction は、それぞれ推論・キャッシュ・テスト、ドキュメント理解、レイアウト計算の正確性を高める
- ターミナルの Emoji は Unicode バージョン、文字幅、複数コードポイントのレンダリング差により予測が難しく、実務では Unicode 9 レベルの単純な Emoji のほうが安定する
ターミナルアニメーションとちらつき制御
- モダンなターミナルエミュレータは古いプロトコルの上で動作しているが、多くの実装はビデオゲームにも使われるグラフィック技術を利用している
- ターミナルの視覚効果はちらつきや tearing が起きやすいが、いくつかの条件を守れば滑らかなアニメーションを実現できる
- ちらつきを減らす最初の方法は、「clear してから draw」ではなく既存内容を上書きすること
- 画面を消去してから新しい内容を追加すると、空のフレームや一部だけ描画されたフレームが一瞬見えることがある
- 2つ目の方法は、更新内容を標準出力へ単一の writeで書き込むこと
file.write を複数回呼ぶと、部分的な更新がユーザーに見えてしまう可能性がある
- 3つ目の方法は Synchronized Output プロトコルを使うこと
- フレームの開始と終了をターミナルに知らせると、ターミナル側がちらつきのない更新に活用できる
- Textual は 60fps を基準にしており、それ以上では目に見える差は大きくないかもしれない
- すべてのアニメーションに同じ価値があるわけではない
- 左からスライドしてくるサイドバーのような効果は見栄えはよいが、ユーザー体験に実質的な利点がない場合がある
- 滑らかなスクロールは、長いテキストで現在位置を維持するのに役立つ
- Textual はこうした理由から、特定のアニメーションをオフにする仕組みを用意する予定
DictView でレンダリング差分を見つける
- Python の
dict の keys() と items() は、それぞれ KeysView、ItemsView を返し、集合に似たインターフェースを提供する
- Textual のレイアウト処理は、Widget と画面位置の対応であるレンダーマップを生成する
- 以前の方式では、Widget 1つの位置が変わっただけでも画面全体を無駄に更新していた
- 2つの
ItemsView の対称差を使うと、新しく作られた項目や変更された項目を得られる
- この演算は C レベルで処理される
- Textual は CSS プロパティが変わったとき、変更された画面領域を見つけて最適化された更新に活用する
lru_cache と小さな関数の最適化
functools 標準ライブラリの @lru_cache は、関数の戻り値をキャッシュするデコレータ
maxsize を設定すると、キャッシュが無限に大きくなる状況を制限できる
- CPython の
lru_cache は C 実装を使っており、キャッシュヒット時もミス時も高速
- Textual には遅くはないが非常に頻繁に呼ばれる小さな関数があり、その多くはキャッシュに適していた
- 通常は
maxsize を 1000〜4000 程度に設定すれば、呼び出しの大半をキャッシュヒットにするのに十分だった
- キャッシュを使うときは
cache_info() の確認が必要
- 効果的なキャッシュなら、
hits が misses より速く増えていく様子が期待できる
- 2つの矩形領域の重なり部分を計算する
intersection メソッドは、作業量自体は大きくないが数千回呼ばれるため、キャッシュの恩恵を受ける
イミュータブルオブジェクトがもたらす単純さ
- Python には本当の意味でのイミュータブルオブジェクトはないが、tuple、
NamedTuple、frozen dataclass によって多くの利点を得られる
- オブジェクトを変更できないという制約は恣意的な制限のように見えるかもしれないが、実際のコードでは役立つことが多い
- Textual でイミュータブルオブジェクトを使うコードは推論しやすく、キャッシュやテストにも有利
- 主な理由は、副作用のないコードを書きやすくなるため
- クラスインスタンスを関数に渡す場合、同じ方法で副作用を避けるのは難しい
Unicode アートとドキュメント化
- 技術的な内容を言葉だけで説明するのが難しいとき、Unicode の罫線文字で作った図はドキュメント理解に大きく役立つ
- Textual の docstring では、領域を4つの下位領域に分割するメソッドを説明するために罫線文字の図を使っている
- こうした図はよく書かれた docstring の代わりにはならないが、併用すると理解度を高められる
- 図の作成ツールとして monodraw を使っている
- Monodraw は macOS 専用
- 他のプラットフォームにも代替手段があるかもしれない
fractions.Fraction でレイアウト誤差を減らす
- Python 標準ライブラリの
fractions モジュールは Python 2.6 から存在する
Fraction は数値を表現する別の方法で、作成後は float の代わりに使える
- 浮動小数点にはよく知られた限界がある
- 例えば
0.1 + 0.1 + 0.1 == 0.3 は False
- Textual では、画面を複数の比率で分割するレイアウトで浮動小数点の丸め誤差が問題になった
- 画面幅の 1/3 のパネルと、残りの 2/3 をさらに分割する状況で、1文字分の隙間ができることがあった
- float を
Fraction に変えると、この種の丸め誤差を避けられる
Fraction(1, 10) 3つの合計は Fraction(3, 10) と等しい
- 24文字を7つの部分に分割する例では、float 版は1文字足りなかったが、Fraction 版は全体の長さを埋めた
ターミナル Emoji の難しさ
- Emoji 対応は Rich の初期から続く問題で、Textual も同じ問題を受け継いでいる
- ターミナルでは文字は通常1セルまたは2セルを占め、一部の文字は幅が0
- CJK 文字は西洋アルファベットの2倍の空間を占める
- 中央揃えや罫線描画のような基本的なフォーマットを行うには、出力されるテキストのターミナル上の幅を知る必要がある
- したがって
len(text) だけでは、ターミナルで占める幅は分からない
- Unicode データベースには単一幅文字と倍幅文字のマッピングがある
- Rich と Textual は、出力するすべての文字についてこのデータベースを参照する
- この処理は軽くはないが、エンジニアリングとキャッシュによって十分高速にできる
- Emoji は Unicode データベースに含まれていても、新しい Unicode リリースごとに新しい Emoji が追加されるため、結果を予測しにくい
- 新しい Emoji をターミナルに出力すると、1セルまたは2セルで表示されることがある
- 正しくレンダリングされないこともある
- ターミナルエミュレータが使っている Unicode バージョンを安定して検出する標準的な方法はない
- 標準の環境変数もない
- 特定のシーケンスを出力してカーソル位置を問い合わせるヒューリスティックはあるが、テストでは Unicode データベースを把握していると思っても、ターミナルが Emoji を予測不能にレンダリングした
- 複数コードポイントの Emoji は問題をさらに複雑にする
- 例えば 👨🏻🦰 は4つのコードポイントで構成される
- あるターミナルでは、これを4文字、2文字、1文字、1セルまたは2セル、あるいは4つの
? としてレンダリングすることがある
- 実務上は Unicode 9 の Emoji を使うと、複数のプラットフォームで比較的安定する
- 新しい Emoji と複数コードポイント文字は、現在のターミナルでよく見えても避けたほうがよい
Textualize の採用
- Textualize は、ブラウザの役割の一部を置き換えられる TUI フレームワークを作るために採用を行っている
- 採用情報は Jobs で確認できる
1件のコメント
Hacker News の意見
すべての TUI 開発者が結局 Unicode にぶつかり、国際文字や絵文字を正しく扱うことが、元の TUI プロジェクトと同じくらい、あるいはそれ以上に大きな別プロジェクトになってしまうのは面白い
rivo/tview でも同じことがあり、その結果作った rivo/uniseg パッケージを通じて、ほかの TUI ライブラリのメンテナーたちも同じ問題を抱えていることが分かった
結局、文字幅は標準化されておらず、ターミナルは複雑なので、誰もがそれぞれの解決策を発明している。記事の筆者は Unicode 9 のみをサポートしているが、現在の Unicode は 15.1 なので、いつか特定の絵文字や国際文字が誤ってレンダリングされるという不満が出るはずで、良い解決策なのかは確信しにくい
この手法を使う TUI ライブラリを 2 つメンテナンスしているが、絵文字サポートはほぼ素晴らしかった。そのうち 1 つは uniseg ライブラリも使用している
https://mitchellh.com/writing/grapheme-clusters-in-terminals
textual に対する最大の不満は、React のようになろうとしている点だ。多くの人が慣れている人気フレームワークなので、なぜそうするのかは分かるが、ユーザーインターフェースを作る良い方法だとは思わない
リアクティブ設計そのものは実証済みの道であり、うまく機能することが知られている方式にシステム設計を寄せるのは、プロジェクトのリスクを下げる良い方法ではある
しかしそこに CSS の変種まで使うのは行き過ぎに見える。React モデルは標準化されたコンポーネントを好む一方で、CSS の概念を多くの面ですでに壊しており、開発者が CSS でコンポーネントをカスタマイズするのは、正当なアーキテクチャというより React の進化の副作用のように見える
最後に使ったときは CSS を使う必要があり、良い標準コンポーネントがなく自分で作らなければならなかった。コンポーネントがきれいにカプセル化された 1 つの Python クラスになる代わりに、公式ドキュメントではリストコンポーネントを作り、外部スタイルシートで装飾する形だった
Python では、何かを行う明白な方法が 1 つ、できればただ 1 つあるべきだ。React をあまりに近く追いかけることで、JavaScript コミュニティの最大の悪習まで真似しているように見える
良い標準コンポーネントがないという点も、確認してからかなり経っているようだ: https://textual.textualize.io/widget_gallery/
自分でコンポーネントを作る必要もなく、CSS はすでに知っていた。むしろ、少しの レイアウトの違いや CSS から外れた部分が時々混乱を招いた
通常の GUI ウィンドウ内の 1 コンポーネントがターミナルウィンドウになっている形を想像すればよい。こういうものがあるのか、それとも単に等幅フォントのテキストビューを使うべきなのか気になる
このTUIは見た目はきれいだが、実際に自分が使ってお金まで払う状況は想像しにくい。適した環境にいないだけかもしれないが、自分の経験では、人々は本当にミニマルなツールで満足するか、すぐにGUIでユーザーを満足させようとする
たとえば記事のYouTubeリンクでは、セルを強調表示する表が示されていたが、なぜそれをTUIでやる必要があるのか分からない。強調されたアクティブセルを移動しなければならないなら、結局ほかにも多くの機能が必要になり、最終的にはきちんとしたGUIが必要になりそうだ
ただし「きちんとしたGUI」が何を指すのかはよく分からない。ターミナルには広く使われている公開標準があり、これらのプロトコルはどんなGUIフレームワークよりも標準的で相互運用性が高く、あらゆるシステムでサポートされている
ここに動画のような要素が入り、いくつかの穴が埋まれば、ブラウザと競合したり、コンピューティング体験全体をターミナルに持ち込んだりするのも合理的に見える
もちろんターミナルが動画を追加すれば、「きちんとしたGUI」のレンダリングパイプラインをまねることになるが、ターミナル体験を改善することであり、大きく成功すればGUIにUNIX的なシンプルさをもたらすものとも見なせる
従業員はキーボードショートカットで作業していて非常に効率的で、誰かがAS400を現代的なWebアプリに置き換えようとするたびに生産性が100倍落ちる
Vim/Emacsの達人と、見た目は洗練されているがキーボードショートカットのないGUIとの差のような状況だ
ユーザーにサーバーへSSHで接続させ、TUIアプリをシェルのように使わせることはできるが、かなり無理のあるシナリオであることは認める
個人的にはTextualは使うのが少し奇妙だったが、ncursesよりは良かった。ただし望む結果は得られなかった。昔のメインフレーム式TUIアプリケーションは非常に効率的に感じられた
ときにはうまく動くが、たとえば帯域幅の大きい接続でXフォワーディングを使う場合のように限定的だ。それ以外の場合、GUIは完全にもっさりする
テキストベースのGUIは両方の利点を取り込めるように見える
「最初のコツは消さずに上書きする」という方式は、DirectX以前のゲームが書かれていた方式と同じだ
フレームバッファに直接書き込み、全体を消して描き直す代わりに、変わった部分とその周辺・下にある部分を描き直す。画面全体を時間内に更新する余裕がなかったからだ
一つ目は別のバッファに書き込み、フレームの終わりにバッファ全体を一度にコピーして、各ピクセルが一度だけ変わるようにする方式だ。このバッファは通常、ビデオRAMではなく通常のRAMである場合もあった
二つ目はビデオRAM内の別のバッファに書き込み、グラフィックハードウェアのレジスタを変更して、モニターに表示するピクセルをそのバッファから生成させる方式だ
トレードオフは異なっていた。バッファ全体のコピーは高コストで、アドレスレジスタの変更は安価だった。しかしレジスタの詳細はハードウェア依存の場合があり、まともなグラフィックドライバーフレームワークもなかった
単に「バッファを切り替える」には、画面外バッファへレンダリングするときでもピクセルをビデオRAMへ送る必要があったが、ビデオRAMへのアクセスは通常のRAMより遅かったと記憶している。実質的にNUMA構造なので、ピクセルがどれだけ頻繁に上書きされるかによっては、最後のコピーを含めても通常RAMでレンダリングするほうが全体として速いこともあり得た
ただし、ターミナルにフレームの開始と終了を知らせられる能力のほうがはるかに強力だと思う
ソフトウェアエンジニアがなぜTUIにそれほど関心を持つのか、本当に分からない。良いコマンドラインプログラムは好きだが、TUIには惹かれない
しかもハッカーっぽさがある。スタイルがコンテンツから強く分離されておらず、調整可能なスタイルオプションも限られているため、TUIアプリはおおむね一つのスタイルへ収束する。そのスタイルがハッカー映画や格好いいSFを思い起こさせる
企業デザイナーが好む領域でもなく、企業がTUIを根拠に法人や消費者への販売を成立させるわけでもない。そうした商業的圧力がないため、TUIは開発者が開発者のために設計する
そのためTUIミームは自己強化される。開発者はTUIを見て使い、たいてい開発者を念頭に置いたツールだと感じ、また自分だけのTUIを作りたくなる
クロスプラットフォーム性はほぼ無料で、ピクセル単位で完璧なGUIを作る必要がなく、そもそも不可能で、アイコンやグラフィックも不要だ。グラフィックを表示できないからだ
好みのフォントを使うターミナルエミュレーターを使い続けながら、GUIのようなインターフェースを持てる
これまで使ったTUIアプリはどれも応答時間が短かった。GUIアプリもたいてい速いが、イライラするほど遅かったアプリはすべてGUIアプリだった
Monodrawの代替としてascii-drawがある
https://github.com/Nokse22/ascii-draw
TurboVision 以降に見たもののほとんどは、インスピレーションは受けていたものの、実際には完成度が足りなかった。ツールキットをきちんと使ってみると、かわいらしくはあるが未完成のように感じる
別の言い方をすれば、私が見てきた多くの TUI フレームワークは「主張の強い」フレームワークだったとも言える
もしかすると自分が鈍くて創造性に欠けるだけかもしれないが、90年代に DOS から Linux へ移って以来、B800 で作業していた頃ほど生産的だったことはない
ダブルバッファリングの必要性、ローカルの直接テキストモードとターミナルの違い、エスケープシーケンスまで理解するのに時間がかかった可能性はある。それでも ncurses で何かを直接やろうとするたびに、はっきりした不満からほとんど諦めていた。B800 の単純な理想でできていたこととはまったく違っていた
自分の視界が街一つ分ほど遮られているのかもしれないが、メッセージポンプ以降の GUI フレームワークはどれもまったく理解しにくかった
特に、オブジェクト指向フレームワークがマルチスレッド UI の問題を実際にどう解決してくれるのか理解できなかった。Win32 ではメインレンダラーをスレッドに入れ、補助レンダラーが横でモデルを作り、その差分だけ更新していた。コードが特に手に負えないほど大きくなることはなかった
kitty を動かすつもりなら、それよりはるかに多くのことができる: https://m.youtube.com/watch?v=ft1Q-DwGWIs
https://notcurses.com/
どちらも同じプロトコルを使ってグラフィックを表示できる。本当にグラフィックが必要なら、ターミナルはほぼ正しい解決策ではない。ただ、アイコンのような小さいものには時々便利だ
採用中という点が興味深い。TUI フレームワークでどうやって収益を上げるつもりなのか気になる
参考までに、コンパイルされる TUI ライブラリを12個ほど評価してみたが、FTXUI が最も使いやすく安定していた
https://github.com/ArthurSonzogni/FTXUI
キーボードとマウスの両方をサポートするインタラクティブなダッシュボードを作るのに良いツールだ