Iocraft

TUITerminalReact-likeDeclarativeJSX

GitHub概要

ccbrown/iocraft

A Rust crate for beautiful, artisanally crafted CLIs, TUIs, and text-based IO.

スター679
ウォッチ4
フォーク15
作成日:2024年9月2日
言語:Rust
ライセンス:Apache License 2.0

トピックス

cliiocraftlogsrustterminaltui

スター履歴

ccbrown/iocraft Star History
データ取得日時: 2025/7/25 11:09

Iocraft

Iocraftは、ReactのコンセプトをRustのTUI開発に持ち込んだ革新的なライブラリです。JSX風の構文、コンポーネントベースの設計、フックの使用など、Reactの良さをターミナルアプリケーション開発に活かすことができます。

特徴

ReactライクなAPI

  • JSX風構文: マクロを使ったJSXライクな構文
  • コンポーネント: 関数コンポーネントのサポート
  • フック: useState、useEffectなどのフックAPI
  • Props: コンポーネント間のデータ受け渡し
  • コンテキスト: React Contextのような状態管理

宣言的UI

  • 仮想DOM: 効率的な差分レンダリング
  • 自動再レンダリング: 状態変更時の自動UI更新
  • イベントハンドリング: イベントの宣言的な処理
  • レイアウト: Flexboxライクなレイアウトシステム

開発者体験

  • ホットリロード: 開発中の自動リロード
  • デバッグツール: React DevTools風のデバッグ機能
  • 型安全: Rustの型システムによる安全性
  • エラーハンドリング: エラーバウンダリのサポート

モダンな機能

  • 非同期対応: async/awaitのサポート
  • サスペンス: 非同期コンポーネントのサポート
  • ポータル: コンポーネントの動的な配置
  • カスタムフック: 独自フックの作成

基本的な使用方法

インストール

[dependencies]
iocraft = "0.5"
tokio = { version = "1", features = ["full"] }

Hello World

use iocraft::prelude::*;

#[component]
fn App() -> impl Into<AnyElement> {
    element! {
        Box(padding: 2, border_style: BorderStyle::Round) {
            Text(content: "Hello, Iocraft!")
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    let mut terminal = Terminal::new()?;
    terminal.run(App).await?;
    Ok(())
}

コンポーネントの作成

use iocraft::prelude::*;

#[derive(Props, Default)]
struct ButtonProps {
    label: String,
    #[prop(default = || Box::new(|_| {}))]
    on_click: Handler<'static, ()>,
}

#[component]
fn Button(props: &ButtonProps) -> impl Into<AnyElement> {
    let ButtonProps { label, on_click } = props;
    
    let (is_hovered, set_is_hovered) = use_state(|| false);
    
    let style = if is_hovered {
        Style::default().fg(Color::Yellow).bold()
    } else {
        Style::default()
    };
    
    element! {
        Box(
            border_style: BorderStyle::Round,
            padding: 1,
            on_mouse_enter: move |_| set_is_hovered(true),
            on_mouse_leave: move |_| set_is_hovered(false),
            on_click: on_click.clone()
        ) {
            Text(content: label.clone(), style)
        }
    }
}

#[component]
fn App() -> impl Into<AnyElement> {
    let (count, set_count) = use_state(|| 0);
    
    element! {
        Flex(direction: FlexDirection::Column, gap: 1) {
            Text(content: format!("Count: {}", count))
            Button(
                label: "Increment".to_string(),
                on_click: move |_| set_count(count + 1)
            )
            Button(
                label: "Decrement".to_string(),
                on_click: move |_| set_count(count.saturating_sub(1))
            )
        }
    }
}

フックの使用

use iocraft::prelude::*;
use std::time::Duration;

#[component]
fn Timer() -> impl Into<AnyElement> {
    let (seconds, set_seconds) = use_state(|| 0);
    
    // useEffect相当
    use_effect(move || {
        let handle = tokio::spawn(async move {
            loop {
                tokio::time::sleep(Duration::from_secs(1)).await;
                set_seconds.update(|s| s + 1);
            }
        });
        
        // Cleanup function
        move || {
            handle.abort();
        }
    });
    
    element! {
        Box(border_style: BorderStyle::Single) {
            Text(content: format!("Timer: {} seconds", seconds))
        }
    }
}

フォーム入力

use iocraft::prelude::*;

#[component]
fn LoginForm() -> impl Into<AnyElement> {
    let (username, set_username) = use_state(String::new);
    let (password, set_password) = use_state(String::new);
    let (message, set_message) = use_state(String::new);
    
    let handle_submit = move |_| {
        if username.is_empty() || password.is_empty() {
            set_message("Please fill in all fields".to_string());
        } else {
            set_message(format!("Welcome, {}!", username));
        }
    };
    
    element! {
        Box(padding: 2, border_style: BorderStyle::Double) {
            Flex(direction: FlexDirection::Column, gap: 1) {
                Text(content: "Login Form", style: Style::default().bold())
                
                Box {
                    Text(content: "Username: ")
                    TextInput(
                        value: username.clone(),
                        on_change: move |new_value| set_username(new_value),
                        width: 20
                    )
                }
                
                Box {
                    Text(content: "Password: ")
                    TextInput(
                        value: password.clone(),
                        on_change: move |new_value| set_password(new_value),
                        password: true,
                        width: 20
                    )
                }
                
                Button(
                    label: "Login".to_string(),
                    on_click: handle_submit
                )
                
                if !message.is_empty() {
                    Text(content: message.clone(), style: Style::default().fg(Color::Green))
                }
            }
        }
    }
}

高度な機能

コンテキストAPI

use iocraft::prelude::*;

#[derive(Clone)]
struct ThemeContext {
    primary_color: Color,
    secondary_color: Color,
}

impl ThemeContext {
    fn dark() -> Self {
        Self {
            primary_color: Color::Blue,
            secondary_color: Color::DarkGray,
        }
    }
    
    fn light() -> Self {
        Self {
            primary_color: Color::Cyan,
            secondary_color: Color::Gray,
        }
    }
}

#[component]
fn ThemedButton(props: &ButtonProps) -> impl Into<AnyElement> {
    let theme = use_context::<ThemeContext>();
    
    element! {
        Box(
            border_style: BorderStyle::Single,
            border_color: theme.primary_color,
            padding: 1
        ) {
            Text(
                content: props.label.clone(),
                style: Style::default().fg(theme.primary_color)
            )
        }
    }
}

#[component]
fn App() -> impl Into<AnyElement> {
    let (is_dark, set_is_dark) = use_state(|| true);
    let theme = if is_dark { ThemeContext::dark() } else { ThemeContext::light() };
    
    element! {
        ContextProvider(value: theme) {
            Flex(direction: FlexDirection::Column, gap: 1) {
                ThemedButton(label: "Themed Button".to_string())
                Button(
                    label: "Toggle Theme".to_string(),
                    on_click: move |_| set_is_dark(!is_dark)
                )
            }
        }
    }
}

カスタムフック

use iocraft::prelude::*;
use std::collections::HashMap;

fn use_local_storage<T: serde::Serialize + serde::de::DeserializeOwned + Clone + 'static>(
    key: &str,
    initial_value: T,
) -> (T, impl Fn(T)) {
    let (value, set_value) = use_state(move || {
        // ローカルストレージから読み込み
        if let Ok(stored) = std::fs::read_to_string(format!(".iocraft_{}", key)) {
            serde_json::from_str(&stored).unwrap_or(initial_value)
        } else {
            initial_value
        }
    });
    
    let key = key.to_string();
    let set_with_storage = move |new_value: T| {
        // ローカルストレージに保存
        if let Ok(serialized) = serde_json::to_string(&new_value) {
            let _ = std::fs::write(format!(".iocraft_{}", key), serialized);
        }
        set_value(new_value);
    };
    
    (value, set_with_storage)
}

リストの仮想化

#[component]
fn VirtualList(props: &VirtualListProps) -> impl Into<AnyElement> {
    let (scroll_offset, set_scroll_offset) = use_state(|| 0);
    let visible_items = 10;
    
    let start_index = scroll_offset;
    let end_index = (start_index + visible_items).min(props.items.len());
    
    element! {
        Box(
            height: visible_items,
            on_scroll: move |event| {
                set_scroll_offset(event.offset);
            }
        ) {
            for i in start_index..end_index {
                ListItem(item: props.items[i].clone(), index: i)
            }
        }
    }
}

エコシステム

関連プロジェクト

  • iocraft-cli: CLIアプリケーションテンプレート
  • iocraft-components: コミュニティ製コンポーネント
  • iocraft-devtools: 開発ツール

学習リソース

  • 公式ドキュメント: 詳細なAPIドキュメント
  • チュートリアル: ステップバイステップの学習ガイド
  • サンプル集: 実用的な例

利点

  • React経験を活用: React開発者がすぐに使える
  • 宣言的: UIの構造が分かりやすい
  • コンポーネント再利用: 効率的な開発
  • 現代的: 最新のUI開発パターン
  • 型安全: Rustの強力な型システム

制約事項

  • 新しい: コミュニティが成長中
  • 学習コスト: ReactとRust両方の知識が必要
  • パフォーマンス: 仮想DOMのオーバーヘッド
  • エコシステム: サードパーティ製コンポーネントが少ない
  • 安定性: APIが変更される可能性

他のライブラリとの比較

項目IocraftRatatuiTUI Realm
パラダイムReactライクイミディエートMVU
学習コスト中(React経験者は低)
柔軟性非常に高
コミュニティ成長中大きい中程度
特徴JSX風構文パフォーマンス構造化

まとめ

Iocraftは、Reactの開発体験をRustのTUI開発に持ち込む革新的なライブラリです。JSX風の構文、コンポーネントベースの設計、フックAPIなど、Reactの優れた概念を活用できます。特に、Reactの経験がある開発者や、モダンなUI開発パターンをターミナルアプリケーションに適用したい開発者にとって理想的な選択です。