6ヶ月前、私はtkinterを使ってPythonの監視ダッシュボードをリリースしていた。動いてはいたが、ギリギリの状態だった。イベントループは脆弱で、クロスプラットフォームのレンダリングは安定せず、新しいウィジェットを追加するたびに2003年製のフレームワークと格闘しているような気分だった。同僚からTextualを教えてもらい、週末でそのダッシュボードを書き直した。それ以来ずっと本番環境で稼働しており、tkinterには一切触れていない。
以下は、Textualで実際のTUIアプリを構築する中で学んだことだ — READMEのサンプルコードではなく、リアクティブな状態管理、ライブデータ、マルチスクリーンナビゲーションを備えた本番グレードの実践的な内容である。
クイックスタート:5分で動作するアプリを作る
devエクストラ付きでTextualをインストールする — CSSインスペクターだけでも十分価値がある:
pip install textual[dev]
最小限で実用的なTextualアプリを示す — ライブシステムモニターの骨格だ:
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
from textual.reactive import reactive
class MonitorApp(App):
CSS = """
Static {
border: solid green;
padding: 1;
margin: 1;
}
"""
def compose(self) -> ComposeResult:
yield Header()
yield Static("CPU: 読み込み中...", id="cpu")
yield Static("メモリ: 読み込み中...", id="mem")
yield Footer()
if __name__ == "__main__":
app = MonitorApp()
app.run()
実行:
python monitor.py
ヘッダー、フッター、スタイル付きボックスを備えたフルスクリーンのターミナルアプリが起動する — 設定不要、Tkルートウィンドウ不要、mainloopのボイラープレートも不要だ。初めて実行した瞬間、tkinterとは終わりだと悟った。
詳細解説:ウィジェット、レイアウト、CSSシステム
レイアウトの仕組み
TextualはターミナルのみでCSSにインスパイアされたレイアウトエンジンを使用している。フロントエンド開発の経験があれば、ポジショニングモデルはすぐに馴染めるはずだ。レイアウトはインライン(クラスレベルのCSS文字列)または外部の.tcssファイルで定義できる。
最もよく使うレイアウトモードはhorizontalとverticalの2つだ。分割パネルレイアウトは以下のようになる:
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
from textual.containers import Horizontal, Vertical
class SplitApp(App):
CSS = """
#sidebar {
width: 30%;
background: $panel;
border-right: solid $accent;
}
#main {
width: 70%;
}
"""
def compose(self) -> ComposeResult:
yield Header()
with Horizontal():
with Vertical(id="sidebar"):
yield Static("ナビゲーション")
yield Static("設定")
with Vertical(id="main"):
yield Static("メインコンテンツエリア")
yield Footer()
if __name__ == "__main__":
SplitApp().run()
$panelと$accent変数はTextualの組み込みテーマから来ている。デフォルトのキーバインドでユーザーがダーク/ライトモードを切り替えると、自動的に適応する。
リアクティブプロパティ:核心となる概念
リアクティブプロパティはTextualで最も重要な概念であり、他のすべてのTUIライブラリとの差別化要因だ。値が変わると自動的にUIの再レンダリングをトリガーするクラスレベルの属性だ。手動のリフレッシュ呼び出しも、イベントのディスパッチも不要。新しい値を代入するだけでウィジェットが更新される。
from textual.app import App, ComposeResult
from textual.widgets import Static
from textual.reactive import reactive
class CounterWidget(Static):
count = reactive(0)
def render(self) -> str:
return f"カウント: {self.count}"
class CounterApp(App):
def compose(self) -> ComposeResult:
yield CounterWidget(id="counter")
def on_mount(self) -> None:
self.set_interval(1, self.increment)
def increment(self) -> None:
self.query_one("#counter", CounterWidget).count += 1
if __name__ == "__main__":
CounterApp().run()
手動リフレッシュなしでカウンターが毎秒インクリメントされる。reactiveはダーティチェックと再レンダリングのスケジューリングを内部で処理する。本番の監視ダッシュボードでこれを6ヶ月間運用し、CPUとメモリとディスクのメトリクスを毎秒読み取り続けたが、メモリリークはゼロ、持続的な負荷下でも更新の欠落はゼロだった。
応用:リアルタイム更新とDataTable
ライブデータのためのバックグラウンドワーカー
リアルタイムデータ(メトリクス、ログ、APIポーリング)には、Textualがworkデコレーターを提供しており、UIスレッドをブロックせずにコルーチンをバックグラウンドタスクとして実行できる:
import asyncio
import psutil
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Static
from textual.reactive import reactive
from textual import work
class CPUWidget(Static):
cpu_pct = reactive(0.0)
def render(self) -> str:
bar = "█" * int(self.cpu_pct / 5)
return f"CPU [{bar:<20}] {self.cpu_pct:.1f}%"
class LiveMonitor(App):
def compose(self) -> ComposeResult:
yield Header()
yield CPUWidget(id="cpu")
yield Footer()
def on_mount(self) -> None:
self.poll_metrics()
@work(exclusive=True)
async def poll_metrics(self) -> None:
while True:
cpu = psutil.cpu_percent(interval=None)
self.query_one("#cpu", CPUWidget).cpu_pct = cpu
await asyncio.sleep(1)
if __name__ == "__main__":
LiveMonitor().run()
構造化データのためのDataTable
DataTableは大規模な表形式データセットを効率的に処理する — 行を仮想化するため、10,000行のレンダリングコストは10行とほぼ同じだ:
from textual.app import App, ComposeResult
from textual.widgets import DataTable
ROWS = [
("プロセス", "PID", "CPU%", "MEM%"),
("nginx", 1234, "0.1", "0.8"),
("postgres", 5678, "2.3", "12.4"),
("python", 9101, "18.7", "3.2"),
]
class TableApp(App):
def compose(self) -> ComposeResult:
yield DataTable()
def on_mount(self) -> None:
table = self.query_one(DataTable)
table.add_columns(*ROWS[0])
table.add_rows(ROWS[1:])
if __name__ == "__main__":
TableApp().run()
ソート可能な列、キーボードナビゲーション、カーソルハイライトが無料で手に入る。コールバック付きの行選択を追加するのはあと2行のコードだ。
Textual DevTools
CSSインスペクターはターミナルフレームワークで見つけるとは思っていなかった機能だ。アプリをこのように実行する:
textual run --dev monitor.py
次に、別のターミナルでインスペクターを開く:
textual console
ホバー時にウィジェットをハイライトし、計算済みスタイルを表示し、CSSをリアルタイムで変更できるライブCSSインスペクターが使える。ブラウザのDevToolsの領域だ — ここで見つけるとは思っていなかった。以前は完全な再起動が必要だったレイアウトの変更が、今では約10秒でテストできる。
6ヶ月の本番運用から得た実践的なヒント
20行を超えたら.tcssファイルを使う
インラインCSSは素早いプロトタイプに適している。それ以上の規模では、スタイルをapp.tcssに分離してこのように参照する:
class MyApp(App):
CSS_PATH = "app.tcss"
エディターのシンタックスハイライトが認識してくれる上に、DevToolsコンソールはアプリを再起動せずにスタイル変更をホットリロードできる。
マルチビューアプリのためのスクリーンベースナビゲーション
マルチページのナビゲーションにはTextualのScreenシステムを使う。スタックのようにスクリーンをプッシュ/ポップできる:
from textual.screen import Screen
from textual.app import App
from textual.widgets import Button
class DetailScreen(Screen):
def compose(self):
yield Button("戻る", id="back")
def on_button_pressed(self, event):
self.app.pop_screen()
class MainApp(App):
def compose(self):
yield Button("詳細を開く", id="detail")
def on_button_pressed(self, event):
self.push_screen(DetailScreen())
これが監視ダッシュボードで設定パネルやドリルダウンビューを扱う方法だ。各ビューは独立したScreenで、キーボードショートカットによりナビゲーションが本物のアプリのように感じられる。
スレッドセーフティに注意
TextualのUIはasyncioのイベントループ上で動作する。通常の(非同期でない)スレッド — たとえばサブプロセスやソケットリスナー — からデータを取得する場合は、app.call_from_thread()を使って安全に更新をポストする:
import threading
def background_reader(app):
# 通常スレッドで実行中
while True:
data = fetch_some_data()
app.call_from_thread(app.update_display, data)
thread = threading.Thread(target=background_reader, args=(app,), daemon=True)
thread.start()
これを省略すると、負荷がかかったときにしか現れない微妙な競合状態が生じる — 私も最初にこれに当たり、1日デバッグに費やした。
キーバインドとFooterウィジェット
クラスレベルでキーバインドを宣言すると、Footerウィジェットが画面下部のヘルプバーとして自動的にレンダリングする:
class MyApp(App):
BINDINGS = [
("q", "quit", "終了"),
("r", "refresh", "更新"),
("d", "toggle_dark", "ダークモード"),
]
def action_refresh(self) -> None:
# カスタム更新ロジック
self.poll_metrics()
メソッド名はaction_<binding_name>と一致する必要がある。action_quitとaction_toggle_darkは組み込み済みだ — 自分で実装する必要はない。
Textualが適切でない場合
Textualはシンプルなスクリプトに対して本当の複雑さをもたらす。ワンショットコマンドにプログレスバーが必要?代わりにRichを使おう — Textualはそもそもその上に構築されている。Textualが複雑さに見合う価値を発揮するのは、持続的なインタラクティビティが必要な場合だ:ダッシュボード、設定マネージャー、ログビューアー、データベースブラウザー。ユーザーがライブデータをナビゲート、フィルター、操作する必要がある場合、それが適切なツールだ。
私の現在の本番スタックでは、3つの環境にまたがるサービスを追跡するデプロイメントモニターにTextualを使っている。キーボードショートカット、ダークモード、マウスサポートを備え、XフォワーディングやVNCなしで完全にSSH経由で動作するため、チームはすぐに採用した。

