ratatui-tui について
ratatui-tuiは、ratatuiでターミナルUIを開発するRust開発者向けのスキルです。最小構成からモジュール型アプリまでのテンプレートに加え、非同期イベント、スタイル、画像統合、エラー処理のパターンを紹介します。
主な機能
- 01複雑さの異なる4種類のアプリテンプレート
- 02TEAによる状態とUIの構成
- 03同期・非同期イベント処理の例
- 04レイアウト・ウィジェット・スタイルのパターン
- 05画像統合とエラー処理のガイド
活用シーン
- 01ターミナルUIのプロトタイプ作成
- 02非同期またはネットワーク対応アプリの開発
- 03複数画面のモジュール型TUI構築
- 04リリース前のビルドとコード品質の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ pedronauck/ skills --skill ratatui-tuiインストール後、Claude Code で/ratatui-tuiを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
assets/templates/async-app/Cargo.lockCargo.tomlassets/templates/async-app/src/main.rsassets/templates/component-app/Cargo.lockCargo.tomlassets/templates/component-app/src/action.rsapp.rsconfig.rsevent.rslogging.rsmain.rstui.rsui.rsassets/templates/hello-world/Cargo.lockCargo.tomlassets/templates/hello-world/src/main.rsassets/templates/simple-app/Cargo.lockCargo.tomlassets/templates/simple-app/src/main.rsreferences/architecture-patterns.mdasync-patterns.mdimage-integration.mdstyle-guide.md詳細
Ratatui TUI 開発
クイックスタート
-
プロジェクトにテンプレートをコピーします。
cp -r ~/.claude/skills/ratatui-tui/assets/templates/<template>/* . -
実行します。
cargo run
テンプレートの選択
| 複雑さ | テンプレート | 用途 |
|---|---|---|
| 最小限 | hello-world | 学習、簡単なデモ |
| シンプル | simple-app | 単一画面のアプリ、ツール |
| 非同期 | async-app | バックグラウンドタスク、ネットワーク |
| フル | component-app | 複数ビュー、設定、ロギング |
選択フロー:
- 非同期処理やネットワークが必要ですか? →
async-app - 複数の画面やコンポーネントが必要ですか? →
component-app - シンプルなツールだけが必要ですか? →
simple-app - ratatui を学習していますか? →
hello-world
プロジェクトのセットアップ
最小限の Cargo.toml
[package]
name = "my-tui"
version = "0.1.0"
edition = "2024"
[dependencies]
ratatui = "0.30"
crossterm = "0.29"
color-eyre = "0.6"
すべての依存関係(component-app)
[dependencies]
ratatui = "0.30"
crossterm = { version = "0.29", features = ["event-stream"] }
color-eyre = "0.6"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
clap = { version = "4", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
serde = { version = "1", features = ["derive"] }
config = "0.15"
dirs = "6"
# Optional: image support
ratatui-image = { version = "5", features = ["chafa-static"] }
リリースプロファイル
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
コアループ:TEA(The Elm Architecture)
Model → Message → Update → View
↑ |
└─────────────────────────┘
struct App {
counter: i32,
should_quit: bool,
}
enum Message {
Increment,
Decrement,
Quit,
}
impl App {
fn update(&mut self, msg: Message) {
match msg {
Message::Increment => self.counter += 1,
Message::Decrement => self.counter -= 1,
Message::Quit => self.should_quit = true,
}
}
fn view(&self, frame: &mut Frame) {
let text = format!("Counter: {}", self.counter);
frame.render_widget(Paragraph::new(text), frame.area());
}
}
スタイルのルール
Stylize トレイトのヘルパーを使用します。
use ratatui::style::Stylize;
// Good
"text".bold()
"text".dim()
"text".cyan()
"text".on_dark_gray()
"text".bold().cyan()
// Avoid
Style::default().fg(Color::White) // hardcoded white
Style::default().fg(Color::Black) // hardcoded black
Style::new().add_modifier(Modifier::BOLD) // verbose
カラーパレット:
- プライマリ:
.cyan()、.green() - エラー:
.red() - 警告:
.yellow()(控えめに使用) - ミュート:
.dim()、.dark_gray() - アクセント:
.magenta()
テキストの折り返し:
use textwrap::wrap;
use ratatui::text::Line;
let wrapped: Vec<Line> = wrap(&long_text, width as usize)
.into_iter()
.map(|cow| Line::from(cow.into_owned()))
.collect();
ウィジェットのパターン
StatefulWidget
struct MyList {
items: Vec<String>,
}
struct MyListState {
selected: usize,
}
impl StatefulWidget for MyList {
type State = MyListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// render with state.selected
}
}
// Usage
frame.render_stateful_widget(my_list, area, &mut state);
レイアウト
let [header, main, footer] = Layout::vertical([
Constraint::Length(1),
Constraint::Fill(1),
Constraint::Length(1),
]).areas(frame.area());
let [left, right] = Layout::horizontal([
Constraint::Percentage(30),
Constraint::Fill(1),
]).areas(main);
組み込みの状態型
ListState- List ウィジェット用TableState- Table ウィジェット用ScrollbarState- Scrollbar 用
参照:references/architecture-patterns.md
非同期イベント処理
use crossterm::event::{EventStream, Event, KeyCode};
use futures::StreamExt;
use tokio::select;
async fn run(mut app: App) -> Result<()> {
let mut events = EventStream::new();
loop {
// Render
terminal.draw(|f| app.view(f))?;
// Handle events
select! {
Some(Ok(event)) = events.next() => {
if let Event::Key(key) = event {
match key.code {
KeyCode::Char('q') => break,
KeyCode::Up => app.update(Message::Up),
KeyCode::Down => app.update(Message::Down),
_ => {}
}
}
}
// Add other channels here (background tasks, timers)
}
if app.should_quit {
break;
}
}
Ok(())
}
参照:references/async-patterns.md
画像の統合
use ratatui_image::{picker::Picker, StatefulImage, Resize};
use std::thread;
// Query terminal protocol support once at startup
let mut picker = Picker::from_query_stdio()?;
// Load and resize in background thread
let (tx, rx) = std::sync::mpsc::channel();
thread::spawn(move || {
let dyn_img = image::open("photo.png").unwrap();
let protocol = picker.new_protocol(dyn_img, area.into(), Resize::Fit(None));
tx.send(protocol).unwrap();
});
// In render, use StatefulImage for efficient redraw
if let Ok(protocol) = rx.try_recv() {
image_state = Some(protocol);
}
if let Some(ref mut img) = image_state {
frame.render_stateful_widget(StatefulImage::default(), area, img);
}
重要なポイント:
- ポータブルなバイナリには
chafa-static機能を使用します - プロトコルの問い合わせはフレームごとではなく、一度だけ行います
- リサイズやエンコードはバックグラウンドスレッドに委譲します
- 再描画時の再エンコードを避けるため、
StatefulImageを使用します
参照:references/image-integration.md
エラー処理
use color_eyre::eyre::Result;
fn main() -> Result<()> {
// Install hooks before anything else
color_eyre::install()?;
// Set panic hook to restore terminal
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = crossterm::terminal::disable_raw_mode();
let _ = crossterm::execute!(
std::io::stdout(),
crossterm::terminal::LeaveAlternateScreen
);
original_hook(panic_info);
}));
run()
}
エラーの伝播:
// Use ? for recoverable errors
let file = std::fs::read_to_string(path)?;
// Use color_eyre context
let config = load_config()
.wrap_err("Failed to load configuration")?;
リリースビルド
cargo build --release
バイナリは target/release/<name> にあります。
サイズの最適化:
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
opt-level = "z" # size over speed
テンプレートの概要
hello-world(約25行)
ratatui::run() を使用した最小限の ratatui デモです。
simple-app(約80行)
同期イベントループ、App 構造体、基本的な描画を備えています。
async-app(約120行)
Tokio ランタイム、EventStream、select! パターンを備えています。
component-app(約300行)
完全なモジュール構成です。
main.rs- エントリーポイントapp.rs- App の状態、更新ロジックevent.rs- イベント処理ui.rs- 描画action.rs- Action enumtui.rs- ターミナルのセットアップconfig.rs- dirs を使用した設定logging.rs- tracing のセットアップ
よく使うパターン
中央揃えのポップアップ
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let [_, center, _] = Layout::vertical([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
]).areas(area);
let [_, center, _] = Layout::horizontal([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
]).areas(center);
center
}
キーバインディングの表示
let help = Line::from(vec![
" q ".bold().cyan(),
"quit ".dim(),
" ↑↓ ".bold().cyan(),
"navigate ".dim(),
" Enter ".bold().cyan(),
"select ".dim(),
]);
ステータスバー
let status = Line::from(vec![
" MODE ".bold().on_cyan(),
format!(" {} items ", count).dim().into(),
]);
チェックリスト
リリース前に確認します。
-
cargo fmt -
cargo clippy --all-featuresで警告がない - テスト以外で
unwrap()を使用していない - パニックフックでターミナルの状態を復元する
-
cargo build --releaseが成功する - 対象のターミナルでテストする
コメント
Markdown 対応