はじめに
こんにちは!本記事では、Pythonで美しくインタラクティブなターミナルユーザーインターフェース(TUI)を構築できる「Textual」ライブラリについて、全12章にわたって詳しくご紹介します。Textualは、従来のCLIとは一線を画す表現力と、Webアプリのようなウィジェット設計、そして高速開発を兼ね備えた今注目のフレームワークです。各章は全て動作可能なPythonコードを掲載しています。さあ、最先端のターミナルアプリ開発を一緒に始めましょう!
第1章:Textualとは?新時代のターミナルUI
Textualは、ターミナル上でGUIライクなインターフェースを気軽に構築できるPython製のフレームワークです。レイアウト設計やイベント処理、カスタムウィジェットなど、Webやモバイル開発で得られる快適さを、ローカルやリモートのターミナルでも再現できます。まずはインストールから始めましょう。
# インストール
!pip install textual
第2章:最小限の“Hello, Textual!”
Textualのパワフルさを感じる最もシンプルな方法は、最小限のアプリを試すことです。この章では、1つのウィジェットのみを表示するhelloアプリを作成し、UIの起動や終了など基本挙動を体験します。Appクラスを継承し、composeでウィジェットをyieldするだけでOKです。
from textual.app import App
from textual.widgets import Static
class HelloTextualApp(App):
def compose(self):
yield Static("Hello, Textual!")
if __name__ == "__main__":
HelloTextualApp().run()
第3章:複数ウィジェットとレイアウト
Textualでは複数のウィジェットを同時に配置し、柔軟な画面設計が可能です。HeaderやFooter、複数のStatic要素を重ねてターミナルアプリの骨組みを作成します。composeメソッドに複数yieldすれば、並べて表示されます。
from textual.app import App, ComposeResult
from textual.widgets import Static, Header, Footer
class MultiWidgetApp(App):
def compose(self) -> ComposeResult:
yield Header()
yield Static("メインコンテンツ")
yield Footer()
if __name__ == "__main__":
MultiWidgetApp().run()
第4章:ボタンとイベント処理
Textualの大きな魅力は、イベント駆動型の開発です。ボタンウィジェットへのクリックイベントをon_ で検知し、状態を動的に変更できます。ここではカウントアップするシンプルな操作を実装します。
from textual.app import App
from textual.widgets import Button, Static
class CounterApp(App):
def __init__(self):
super().__init__()
self.count = 0
def compose(self):
self.display = Static(f"現在の値: {self.count}")
yield self.display
yield Button("+1", id="inc_btn")
def on_button_pressed(self, event):
if event.button.id == "inc_btn":
self.count += 1
self.display.update(f"現在の値: {self.count}")
if __name__ == "__main__":
CounterApp().run()
第5章:インタラクティブな入力フォーム
Textualはテキスト入力や選択肢、ラジオボタンなど、実用的なフォーマットがすぐに作れます。ここでは名前を入力し、ボタン押下時に挨拶を表示するフォームを紹介します。
from textual.app import App
from textual.widgets import Input, Button, Static
class InputApp(App):
def compose(self):
self.msg = Static("")
self.input = Input(placeholder="お名前を入力")
yield self.input
yield Button("送信")
yield self.msg
def on_button_pressed(self, event):
self.msg.update(f"こんにちは、{self.input.value} さん!")
if __name__ == "__main__":
InputApp().run()
第6章:レイアウト管理とグリッドシステム
TextualはターミナルUIにもかかわらず、CSS風のレイアウトやグリッド配置ができます。複数のウィジェットのサイズ比や位置を柔軟に指定することで、見やすく整理された画面を作れます。
from textual.app import App, ComposeResult
from textual.containers import Grid
from textual.widgets import Static
class GridApp(App):
def compose(self) -> ComposeResult:
with Grid():
yield Static("左パネル", id="left")
yield Static("右パネル", id="right")
if __name__ == "__main__":
GridApp().run()
第7章:テーマとスタイリング
Textualでは、各ウィジェットやアプリ全体にCSS類似のスタイル設定が可能。文字色や背景色、ボーダーやフォントサイズなど、視認性や美観を柔軟に調整できます。好みのテーマを自作することも容易です。
from textual.app import App
from textual.widgets import Static
class StyleApp(App):
CSS = """
Screen {
background: #282c34;
color: #d7d7d7;
}
Static {
border: round #5fd7ff;
padding: 1;
background: #44475a;
color: #f1fa8c;
}
"""
def compose(self):
yield Static("美しくカスタマイズできるターミナルUI")
if __name__ == "__main__":
StyleApp().run()
第8章:入力と出力の非同期処理
非同期の操作もTextualなら簡単です。データ取得や長時間処理をasync/awaitで実現しつつ、UIが固まることなく操作できます。ここでは擬似的に待ち時間を加えたデータ更新を行います。
from textual.app import App
from textual.widgets import Button, Static
import asyncio
class AsyncApp(App):
def compose(self):
self.stat = Static("データ待ち")
yield self.stat
yield Button("データ取得")
async def on_button_pressed(self, event):
self.stat.update("取得中...")
await asyncio.sleep(2)
self.stat.update("取得完了!")
if __name__ == "__main__":
AsyncApp().run()
第9章:タブ切替とダイナミック画面遷移
Tabやページ遷移のような操作も、Textualなら画面を切り替えるだけ。アプリケーションに複数セクションがある場合も、ボタンやイベントで柔軟に切り替えられます。
from textual.app import App
from textual.widgets import Button, Static
class TabApp(App):
def compose(self):
self.page = Static("ホーム画面")
yield self.page
yield Button("次へ", id="next")
yield Button("戻る", id="back")
def on_button_pressed(self, event):
if event.button.id == "next":
self.page.update("次のページへ移動しました")
elif event.button.id == "back":
self.page.update("ホーム画面へ戻りました")
if __name__ == "__main__":
TabApp().run()
第10章:リスト表示とスクロール管理
リスト表示・スクロール対応もTextualの得意分野です。大量のデータやログを、ウィジェットのスクロールで快適に閲覧可能にします。ここでは20行のリストを自動生成してスクロール表示します。
from textual.app import App
from textual.widgets import ListView, ListItem, Static
class ListApp(App):
def compose(self):
items = [ListItem(Static(f"項目{i}")) for i in range(1, 21)]
yield ListView(*items)
if __name__ == "__main__":
ListApp().run()
第11章:カスタムウィジェットの作成
Textualでは独自のウィジェットコンポーネント設計もできます。自分だけの複雑な挙動や専用部品をクラスとしてまとめ、複数アプリ間で再利用できます。ここではクリックで色が変わる独自Staticウィジェットを作成します。
from textual.app import App
from textual.widgets import Static
class ColorStatic(Static):
def on_click(self, event):
self.styles.background = "#50fa7b" if self.styles.background != "#50fa7b" else "#ff5555"
class CustomWidgetApp(App):
def compose(self):
yield ColorStatic("クリックして色を変えよう", id="color_sw")
if __name__ == "__main__":
CustomWidgetApp().run()
第12章:アプリケーションの本番運用と拡張性
Textualで作ったアプリはクロスプラットフォームで動作し、本格業務アプリへも容易にスケールアップできます。バージョン管理・CI/CD導入・Web展開(今後対応予定)など、本番運用にも最適です。最後に全要素を盛り込んだダッシュボード例です。
from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, Button, Static
from textual.containers import Grid
class DashboardApp(App):
CSS = """
Static {border: round #bd93f9; padding: 1;}
Button {margin: 1;}
"""
def compose(self) -> ComposeResult:
yield Header(show_clock=True)
with Grid():
yield Static("売上: ¥100,000")
yield Static("ユーザ数: 2,340")
yield Static("在庫: 450個")
yield Button("更新")
yield Footer()
if __name__ == "__main__":
DashboardApp().run()
まとめ
Textualは、Pythonで気軽に高品質なTUIアプリを実現するための革新的なライブラリです。GUIとCLIのいいとこ取りで、業務ツールや趣味開発、アイデアのプロトタイプ作成まで幅広く活用できます。本記事の12章を通し、ぜひ最先端のターミナルアプリ開発を体験し、新たな可能性に触れてみてください。