TextualでモダンなTerminal UI (TUI) を構築する:Pythonスクリプトを次のレベルへ

Python tutorial - IT technology blog
Python tutorial - IT technology blog

スクロールし続けるSSH画面と午前2時の恐怖

午前2時。真っ黒なSSH画面には、サーバーログが滝のように流れ落ちています。フリーズしたクローラーボットの数万行の記録の中から、たった一行のエラーを探そうと目を凝らしていました。この状況でgreptail -fを打ち続けるのは、まさに苦行です。その時、ふと思いました。「この無機質な画面を、CPUやRAM、進捗をリアルタイムで監視できるプロフェッショナルなダッシュボードに変えられないだろうか?」

かつて、cursesライブラリでターミナルUI(TUI)を作るのは、ピクセル単位の座標を手動で管理しなければならず, 非常に困難な作業でした。しかし、Textualに出会ってからすべてが変わりました。このライブラリを使えば、Web制作と同じようにCSSでレイアウトを組み、TableやInputといったウィジェットを配置できます。すべてがターミナル上でスムーズに動作します。

クイックスタート:5分で最初のTUIを作成する

理屈をこねる前に、まずは別次元の「Hello World」アプリをインストールして動かしてみましょう。

pip install textual

app.pyファイルを作成し、以下のコードを貼り付けてください。構造を理解しやすいように最小限にまとめています:

from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Label, Button

class MyFirstTUI(App):
    # ショートカットキーの設定
    BINDINGS = [("d", "toggle_dark", "ライト/ダーク"), ("q", "quit", "終了")]

    def compose(self) -> ComposeResult:
        """UIコンポーネントの定義"""
        yield Header(show_clock=True)
        yield Label("監視システム - ステータス: 実行中...")
        yield Button("サーバーチェック", variant="success")
        yield Footer()

    def action_toggle_dark(self) -> None:
        self.dark = not self.dark

if __name__ == "__main__":
    app = MyFirstTUI()
    app.run()

python app.pyを実行して結果を確認してください。ヘッダー、フッター、そしてマウスでクリック可能なボタンを備えたインターフェースが表示されます。単なるprint()の出力よりも、ずっと洗練されていると思いませんか?

なぜTextualは「ゲームチェンジャー」なのか?

10万件以上のバッチ処理をこなす中で、tqdmだけでは不十分だと気づきました。平均速度、エラー数、そして直近5件のログを同時に確認する必要があったのです。Textualは、非常にスマートなReactiveモデルでこれを解決します。

最大の魅力は、TCSS (Textual CSS) を通じてロジックとデザインを分離できる点です. Pythonのロジックを汚すことなく、色、マージン、パディングを調整できます。例えば、Labelをよりプロフェッショナルに見せるには、style.tcssファイルを使用します:

Label {
    width: 100%;
    height: 3;
    content-align: center middle;
    background: $accent;
    color: $text;
    text-style: bold;
    border: solid $secondary;
}

AppクラスにCSS_PATH = "style.tcss"を追加するだけで、Textualが自動的にレイアウトを計算してくれます。もう画面の調整のために文字数を数える必要はありません。

実践的な監視ダッシュボードの構築

ps auxを繰り返し入力する代わりに、自動更新されるデータテーブルを作成しましょう。これは私がバックグラウンドプロセスを管理している方法です。

from textual.app import App, ComposeResult
from textual.widgets import DataTable, Header, Footer
from textual.containers import Container
import random
import asyncio

class MonitorApp(App):
    CSS = "DataTable { height: 1fr; border: double $primary; }"

    def compose(self) -> ComposeResult:
        yield Header()
        yield Container(DataTable())
        yield Footer()

    def on_mount(self) -> None:
        table = self.query_one(DataTable)
        table.add_columns("サービス", "ステータス", "稼働時間")
        table.add_row("Nginx", "[green]実行中[/green]", "12日間")
        table.add_row("PostgreSQL", "[red]停止中[/red]", "0分")
        self.set_interval(2, self.update_data)

    def update_data(self) -> None:
        table = self.query_one(DataTable)
        uptime = f"{random.randint(1, 60)} 分"
        table.update_cell(row_index=0, column_index=2, value=uptime)

この例では、set_intervalを使用してUIを定期的に更新しています。これは、アプリをフリーズさせずにリアルタイム監視ツールを作成するための重要なテクニックです。

ターミナルUI開発で避けるべき「落とし穴」

TUIの開発はWeb開発とは大きく異なります。私がデバッグに数時間を費やして学んだ3つのポイントを紹介します:

  • ターミナルエミュレータ: すべてのターミナルが24ビットカラーをサポートしているわけではありません。10年前の古いサーバーで実行すると、デザインが崩れる可能性があります。iTerm2Windows TerminalKittyなどの使用を推奨します。
  • イベントループ: Textualはasyncio上で動作します。time.sleep(10)を実行すると、UI全体が即座にフリーズします。requestsの代わりにhttpxのような非同期ライブラリを常に使用してください。
  • 解像度: レイアウトに固定の文字数を使用しないでください。ユーザーがターミナルウィンドウのサイズを変更したときにUIが自動で伸縮するよう、fr(fraction)単位を使用しましょう。

ツールをより「プロフェッショナル」に見せるコツ

ツールを「学生の課題」レベルから脱却させるために、以下の3つのテクニックを適用してみてください:

  1. Rich API: [bold red][blink]などのタグを活用して、重要な警告を強調します。
  2. 入力バリデーション: Inputウィジェットのvalidators属性を使用します。これにより、ユーザーがIPアドレスやポート番号の形式を間違えた際に即座にブロックできます。
  3. RichLog: print()の代わりにRichLogを使用しましょう。他のウィジェットを崩すことなく、履歴をスクロールして確認できるようになります。

管理用スクリプトをTUIに移行してから、深夜のトラブル対応のストレスが大幅に軽減されました。闇雲にコマンドを打つ代わりに、グラフを見てボタンを一つ押すだけで再起動ができるようになったからです。もし、あなたの手元に退屈なPythonスクリプトがあるなら、今日から Textual で新しい命を吹き込んでみてはいかがでしょうか。

Share: