tuika 0.1.0

A small retained-tree terminal UI toolkit — flexbox layout, overlays, focus, and components over ratatui.
Documentation
//! Live component gallery. Run with `cargo run -p tuika --example gallery`
//! (press `q` or `Esc` to quit).
//!
//! Demonstrates the declarative `view!` DSL, the motion components, and the
//! native OSC 9;4 progress indicator — the same building blocks yolop's
//! full-screen renderer uses, with no host application involved.

use std::io;
use std::time::Duration;

use crossterm::event::{self, Event as CtEvent, KeyCode, KeyEventKind};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use ratatui::backend::CrosstermBackend;
use ratatui::text::{Line, Span};
use ratatui::{Terminal, TerminalOptions, Viewport};

use tuika::{Loader, ProgressBar, Spinner, SpinnerStyle, Text, Theme, view};

fn build(frame: u64, theme: &Theme) -> tuika::Element {
    let labeled = |style: SpinnerStyle, label: &str| -> tuika::Element {
        view! {
            row(gap = 1) {
                fixed(1) { node(Spinner::new(frame).style(style)) }
                text(label.to_string())
            }
        }
    };
    let animated = tuika::anim::ping_pong(frame, 120);

    view! {
        col(padding = tuika::Padding::all(1), gap = 1,
            background = ratatui::style::Style::default().bg(theme.background)) {
            fixed(5) {
                boxed(title = Line::from(Span::styled(" spinners ", theme.accent_style()))) {
                    col {
                        fixed(1) { node(labeled(SpinnerStyle::Braille, "Braille")) }
                        fixed(1) { node(labeled(SpinnerStyle::Line, "Line")) }
                        fixed(1) { node(labeled(SpinnerStyle::Dots, "Dots")) }
                    }
                }
            }
            fixed(6) {
                boxed(title = Line::from(Span::styled(" progress ", theme.accent_style()))) {
                    col {
                        fixed(1) { node(ProgressBar::determinate(0.25).percent(true)) }
                        fixed(1) { node(ProgressBar::determinate(0.60).percent(true)) }
                        fixed(1) { node(ProgressBar::determinate(animated).percent(true)) }
                        fixed(1) { node(ProgressBar::indeterminate(frame)) }
                    }
                }
            }
            fixed(3) {
                boxed(title = Line::from(Span::styled(" loader ", theme.accent_style()))) {
                    node(Loader::new(frame, "working…").hint("esc to quit"))
                }
            }
            grow(1) { spacer() }
            fixed(1) {
                node(Text::new(vec![Line::from(Span::styled(
                    "tuika gallery — native progress is live in the taskbar/top bar · press q to quit",
                    theme.muted_style(),
                ))]))
            }
        }
    }
}

fn main() -> io::Result<()> {
    enable_raw_mode()?;
    let mut alt = tuika::AltScreen::enter()?;
    let mut terminal = Terminal::with_options(
        CrosstermBackend::new(io::stdout()),
        TerminalOptions {
            viewport: Viewport::Fullscreen,
        },
    )?;
    let theme = Theme::default();
    let mut progress = tuika::TerminalProgress::new();
    progress.indeterminate();

    let mut frame = 0u64;
    loop {
        terminal.draw(|f| {
            let area = f.area();
            let root = build(frame, &theme);
            tuika::paint(f.buffer_mut(), area, &theme, root.as_ref(), &[]);
        })?;
        if event::poll(Duration::from_millis(80))?
            && let CtEvent::Key(key) = event::read()?
            && key.kind != KeyEventKind::Release
            && matches!(key.code, KeyCode::Char('q') | KeyCode::Esc)
        {
            break;
        }
        frame = frame.wrapping_add(1);
    }

    progress.clear();
    let _ = terminal.clear();
    drop(terminal);
    alt.leave();
    disable_raw_mode()?;
    Ok(())
}