tui-lipan 0.1.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use std::time::Duration;

use tui_lipan::prelude::*;

struct DevtoolsDemo;

#[derive(Default)]
struct State {
    tick: u64,
}

#[derive(Clone)]
enum Msg {
    Tick,
    Quit,
}

impl Component for DevtoolsDemo {
    type Message = Msg;
    type Properties = ();
    type State = State;

    fn create_state(&self, _props: &Self::Properties) -> Self::State {
        State::default()
    }

    fn init(&mut self, _ctx: &mut Context<Self>) -> Option<Command> {
        tui_lipan::debug_log!("[devtools] app init");
        Some(schedule_tick())
    }

    fn on_key(&mut self, key: KeyEvent, ctx: &mut Context<Self>) -> KeyUpdate {
        match key.code {
            KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('Q') => {
                ctx.link().send(Msg::Quit);
                KeyUpdate::handled(Update::none())
            }
            KeyCode::Char('l') | KeyCode::Char('L') => {
                tui_lipan::debug_log!("[devtools] manual log at tick={}", ctx.state.tick);
                KeyUpdate::handled(Update::none())
            }
            _ => KeyUpdate::unhandled(Update::none()),
        }
    }

    fn update(&mut self, msg: Self::Message, ctx: &mut Context<Self>) -> Update {
        match msg {
            Msg::Tick => {
                ctx.state.tick = ctx.state.tick.saturating_add(1);

                if ctx.state.tick % 2 == 0 {
                    tui_lipan::debug_log!(
                        "[devtools] tick={} phase={}",
                        ctx.state.tick,
                        phase_label(ctx.state.tick)
                    );
                }

                Update::with_command(schedule_tick())
            }
            Msg::Quit => {
                tui_lipan::debug_log!("[devtools] quit requested");
                ctx.quit();
                Update::none()
            }
        }
    }

    fn view(&self, ctx: &Context<Self>) -> Element {
        let phase = phase_label(ctx.state.tick);
        let pulse = "*".repeat((ctx.state.tick % 20) as usize + 1);

        VStack::new()
            .padding(1)
            .gap(1)
            .child(
                Frame::new()
                    .title("Devtools Demo")
                    .border(true)
                    .padding(1)
                    .child(
                        VStack::new()
                            .gap(1)
                            .child(Text::new("Press F12 to toggle DevTools overlay"))
                            .child(Text::new("Press L to emit a manual debug log"))
                            .child(Text::new("Press Q or Esc to quit")),
                    ),
            )
            .child(
                Frame::new()
                    .title("Live State")
                    .border(true)
                    .padding(1)
                    .child(
                        VStack::new()
                            .gap(1)
                            .child(Text::new(format!("Tick: {}", ctx.state.tick)))
                            .child(Text::new(format!("Phase: {phase}")))
                            .child(Text::new(format!("Pulse: {pulse}"))),
                    ),
            )
            .into()
    }
}

fn phase_label(tick: u64) -> &'static str {
    match tick % 4 {
        0 => "collect-input",
        1 => "run-update",
        2 => "reconcile-tree",
        _ => "render-frame",
    }
}

fn schedule_tick() -> Command {
    Command::spawn(move |link| {
        std::thread::sleep(Duration::from_millis(350));
        link.send(Msg::Tick);
    })
}

fn main() -> Result<()> {
    App::new()
        .title("tui-lipan - Devtools Demo")
        .mount(DevtoolsDemo)
        .run()
}