TUI Realm

TUITerminalComponentElmDeclarative

GitHub概要

veeso/tui-realm

👑 A ratatui framework to build stateful applications with a React/Elm inspired approach

スター743
ウォッチ2
フォーク32
作成日:2021年4月7日
言語:Rust
ライセンス:MIT License

トピックス

consolecrosstermguiratatuiruststatefulstateful-tuiterminalterminal-appterminal-graphicstuitui-frameworktui-realm

スター履歴

veeso/tui-realm Star History
データ取得日時: 2025/7/25 11:09

TUI Realm

TUI Realmは、React/Elm風のアーキテクチャを採用したコンポーネントベースのTUIフレームワークです。Ratatui上に構築され、宣言的な状態管理と型安全なイベント処理を提供します。Model-View-Update (MVU) パターンにより、複雑なTUIアプリケーションを整理された方法で構築できます。

特徴

MVUアーキテクチャ

  • Model: アプリケーション状態の定義
  • View: UIコンポーネントの描画
  • Update: イベントに基づく状態更新
  • Subscription: 外部イベントの監視

コンポーネントシステム

  • Component: 再利用可能なUIパーツ
  • MockComponent: テスト用のモック実装
  • Props/State: コンポーネントの属性と内部状態
  • Event Handling: 型安全なイベント処理

豊富な機能

  • フォーカス管理: 自動的なフォーカス制御
  • グローバルイベント: アプリ全体のイベント処理
  • サブスクリプション: タイマーや外部イベント
  • アニメーション: 組み込みアニメーション機能

開発者体験

  • 型安全: Rustの型システムを活用
  • テストサポート: モック機能付き
  • デバッグ機能: イベントトレーシング
  • 詳細なドキュメント: 豊富な例とガイド

基本的な使用方法

インストール

[dependencies]
tuirealm = "2.0"
ratatui = "0.30"
crossterm = "0.27"

基本的なアプリケーション

use tuirealm::application::{PollStrategy, Update};
use tuirealm::{Application, Component, Event, EventListenerCfg, Sub, SubClause, SubEventClause};
use tuirealm::props::{Alignment, BorderType, Borders, Color, Props, TextSpan};
use tuirealm::command::{Cmd, CmdResult};
use tuirealm::terminal::TerminalBridge;
use tuirealm::tui::layout::{Constraint, Direction, Layout};

// メッセージ定義
#[derive(Debug, PartialEq)]
enum Msg {
    AppClose,
    IncreaseCounter,
    DecreaseCounter,
}

// コンポーネントID
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
enum Id {
    Counter,
    IncButton,
    DecButton,
}

// モデル定義
struct Model {
    app: Application<Id, Msg, NoUserEvent>,
    counter: isize,
    quit: bool,
    redraw: bool,
    terminal: TerminalBridge,
}

impl Update<Msg> for Model {
    fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
        if let Some(msg) = msg {
            self.redraw = true;
            match msg {
                Msg::AppClose => {
                    self.quit = true;
                    None
                }
                Msg::IncreaseCounter => {
                    self.counter += 1;
                    self.app
                        .attr(&Id::Counter, Attribute::Text, AttrValue::String(
                            format!("Counter: {}", self.counter)
                        ));
                    None
                }
                Msg::DecreaseCounter => {
                    self.counter = self.counter.saturating_sub(1);
                    self.app
                        .attr(&Id::Counter, Attribute::Text, AttrValue::String(
                            format!("Counter: {}", self.counter)
                        ));
                    None
                }
            }
        } else {
            None
        }
    }
}

コンポーネント定義

use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{Alignment, BorderType, Borders, Color, Props, Style};
use tuirealm::tui::widgets::{Block, Paragraph};
use tuirealm::{AttrValue, Attribute, Component, Event, MockComponent, State};

#[derive(MockComponent)]
pub struct Label {
    props: Props,
}

impl Default for Label {
    fn default() -> Self {
        Self {
            props: Props::default(),
        }
    }
}

impl Component<Msg, NoUserEvent> for Label {
    fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
        match ev {
            Event::Keyboard(KeyEvent {
                code: KeyCode::Esc, ..
            }) => Some(Msg::AppClose),
            _ => None,
        }
    }

    fn attr(&mut self, attr: Attribute, value: AttrValue) {
        self.props.set(attr, value);
    }

    fn state(&self) -> State {
        State::None
    }

    fn perform(&mut self, _: Cmd) -> CmdResult {
        CmdResult::None
    }
}

// レンダリング実装
impl Component<Msg, NoUserEvent> for Label {
    fn render(&self, render: &mut Frame, area: Rect) {
        let text = self
            .props
            .get_or(Attribute::Text, AttrValue::String(String::default()))
            .unwrap_string();
        
        let widget = Paragraph::new(text)
            .block(
                Block::default()
                    .borders(Borders::ALL)
                    .border_type(BorderType::Rounded)
            )
            .alignment(Alignment::Center);
            
        render.render_widget(widget, area);
    }
}

フォームの実装

use tuirealm::props::{InputType, TextSpan};
use tuirealm::command::{Cmd, Direction, Position};

#[derive(MockComponent)]
pub struct Input {
    props: Props,
    states: OwnStates,
}

#[derive(Default)]
struct OwnStates {
    input: String,
    cursor: usize,
}

impl Component<Msg, NoUserEvent> for Input {
    fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
        match ev {
            Event::Keyboard(KeyEvent {
                code: KeyCode::Char(ch),
                ..
            }) => {
                self.states.input.insert(self.states.cursor, ch);
                self.states.cursor += 1;
                Some(Msg::TextChanged(self.states.input.clone()))
            }
            Event::Keyboard(KeyEvent {
                code: KeyCode::Backspace,
                ..
            }) => {
                if self.states.cursor > 0 {
                    self.states.cursor -= 1;
                    self.states.input.remove(self.states.cursor);
                    Some(Msg::TextChanged(self.states.input.clone()))
                } else {
                    None
                }
            }
            Event::Keyboard(KeyEvent {
                code: KeyCode::Left,
                ..
            }) => {
                self.states.cursor = self.states.cursor.saturating_sub(1);
                None
            }
            Event::Keyboard(KeyEvent {
                code: KeyCode::Right,
                ..
            }) => {
                if self.states.cursor < self.states.input.len() {
                    self.states.cursor += 1;
                }
                None
            }
            _ => None,
        }
    }
    
    fn state(&self) -> State {
        State::One(StateValue::String(self.states.input.clone()))
    }
}

高度な機能

サブスクリプション

// タイマーサブスクリプション
let sub = Sub::new(
    SubEventClause::Tick,
    SubClause::Always
);

app.mount(
    Id::Clock,
    Box::new(ClockComponent::default()),
    vec![sub],
)?;

// ポート監視サブスクリプション
let sub = Sub::new(
    SubEventClause::User(UserEvent::PortData),
    SubClause::Always
);

グローバルリスナー

// グローバルキーバインド設定
app.active_global_listener(
    Id::GlobalListener,
    Box::new(GlobalListener::default()),
    Sub::new(
        SubEventClause::Keyboard(KeyCode::Char('q')),
        SubClause::Always
    )
)?;

アニメーション

use tuirealm::props::Color;
use std::time::Duration;

// アニメーション付きプログレスバー
#[derive(MockComponent)]
pub struct AnimatedProgress {
    props: Props,
    progress: f64,
    animation_step: f64,
}

impl Component<Msg, NoUserEvent> for AnimatedProgress {
    fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
        match ev {
            Event::Tick => {
                if self.progress < 1.0 {
                    self.progress += self.animation_step;
                    self.props.set(
                        Attribute::Value,
                        AttrValue::Number(self.progress)
                    );
                    Some(Msg::Redraw)
                } else {
                    Some(Msg::AnimationComplete)
                }
            }
            _ => None,
        }
    }
}

エコシステム

標準コンポーネント

  • tuirealm-stdlib: 標準的なUIコンポーネント集
  • カスタムコンポーネント: 独自コンポーネントの作成が容易

実例プロジェクト

  • termscp: ターミナルSCP/SFTPファイル転送クライアント
  • rustmission: Transmission BitTorrentクライアント
  • music-player: ターミナル音楽プレーヤー

設計原則

  1. 宣言的UI: UIの状態を宣言的に記述
  2. 型安全: コンパイル時にエラーを検出
  3. コンポーネント指向: 再利用可能なUI部品
  4. テスタビリティ: モックとテストのサポート

利点

  • 構造化: MVUパターンによる整理された設計
  • 型安全: Rustの型システムを最大限活用
  • 再利用性: コンポーネントベースの設計
  • テスト容易性: モック機能による単体テスト
  • 拡張性: カスタムコンポーネントの作成が簡単

制約事項

  • 学習曲線: MVUパターンの理解が必要
  • ボイラープレート: コンポーネント定義に多くのコード
  • パフォーマンス: 抽象化レイヤーのオーバーヘッド
  • 依存関係: Ratatuiに依存

他のライブラリとの比較

項目TUI RealmRatatuiCursive
アーキテクチャMVUイミディエートリテインド
抽象度
学習コスト低〜中
再利用性非常に高
テスト性非常に高

まとめ

TUI Realmは、React/Elmの開発経験を活かしてRustでTUIアプリケーションを構築したい開発者に最適です。MVUパターンとコンポーネントシステムにより、複雑なアプリケーションでも管理しやすい構造を実現できます。特に、大規模なTUIアプリケーションや、チーム開発において真価を発揮します。