a3s-tui 0.1.14

TEA (The Elm Architecture) framework for terminal user interfaces
Documentation
use a3s_tui::cmd;
use a3s_tui::components::{Alert, AlertKind, TextInput};
use a3s_tui::element::{BoxElement, FlexDirection, TextElement};
use a3s_tui::style::Color;
use a3s_tui::{col, Element, ElementModel, ElementProgramBuilder, Event, KeyCode};
use std::time::Duration;

struct FormApp {
    name: TextInput,
    email: TextInput,
    password: TextInput,
    focused_field: usize,
    submitted: bool,
    error: Option<String>,
}

enum Msg {
    Quit,
    NextField,
    PrevField,
    Submit,
    KeyPress(Event),
}

impl From<Event> for Msg {
    fn from(event: Event) -> Self {
        match &event {
            Event::Key(k) if k.is_ctrl('q') => Msg::Quit,
            Event::Key(k) if k.is_key(KeyCode::Tab) => Msg::NextField,
            Event::Key(k) if k.is_key(KeyCode::BackTab) => Msg::PrevField,
            Event::Key(k) if k.is_ctrl('s') => Msg::Submit,
            _ => Msg::KeyPress(event),
        }
    }
}

impl ElementModel for FormApp {
    type Msg = Msg;

    fn init(&mut self) -> Option<cmd::Cmd<Msg>> {
        self.name.focus();
        None
    }

    fn update(&mut self, msg: Msg) -> Option<cmd::Cmd<Msg>> {
        match msg {
            Msg::Quit => Some(cmd::quit()),
            Msg::NextField => {
                self.blur_all();
                self.focused_field = (self.focused_field + 1) % 3;
                self.focus_current();
                None
            }
            Msg::PrevField => {
                self.blur_all();
                self.focused_field = if self.focused_field == 0 {
                    2
                } else {
                    self.focused_field - 1
                };
                self.focus_current();
                None
            }
            Msg::Submit => {
                if self.validate() {
                    self.submitted = true;
                    Some(cmd::delay(Duration::from_secs(2), Msg::Quit))
                } else {
                    None
                }
            }
            Msg::KeyPress(event) => {
                if let Event::Key(key_event) = event {
                    match self.focused_field {
                        0 => {
                            self.name.handle_key(&key_event);
                        }
                        1 => {
                            self.email.handle_key(&key_event);
                        }
                        2 => {
                            self.password.handle_key(&key_event);
                        }
                        _ => {}
                    }
                }
                None
            }
        }
    }

    fn view(&self) -> Element<Msg> {
        let title = Element::Text(
            TextElement::new("User Registration Form")
                .bold()
                .fg(Color::Cyan),
        );

        let name_label = Element::Text(TextElement::new("Name:").fg(Color::BrightBlack));
        let name_input = self.name.element();

        let email_label = Element::Text(TextElement::new("Email:").fg(Color::BrightBlack));
        let email_input = self.email.element();

        let password_label = Element::Text(TextElement::new("Password:").fg(Color::BrightBlack));
        let password_input = self.password.element();

        let help = Element::Text(
            TextElement::new("Tab: next | Shift+Tab: prev | Ctrl+S: submit | Ctrl+Q: quit")
                .fg(Color::gray(150)),
        );

        let mut content = col![
            Element::Text(TextElement::new("")),
            title,
            Element::Text(TextElement::new("")),
            name_label,
            name_input,
            Element::Text(TextElement::new("")),
            email_label,
            email_input,
            Element::Text(TextElement::new("")),
            password_label,
            password_input,
            Element::Text(TextElement::new("")),
            help,
        ];

        if let Some(ref err) = self.error {
            content = col![
                content,
                Element::Text(TextElement::new("")),
                Alert::new(AlertKind::Error, err).element(),
            ];
        }

        if self.submitted {
            content = col![
                content,
                Element::Text(TextElement::new("")),
                Alert::new(AlertKind::Success, "Registration successful!")
                    .title("Success")
                    .element(),
            ];
        }

        Element::Box(
            BoxElement::new()
                .direction(FlexDirection::Column)
                .padding(2)
                .child(content),
        )
    }
}

impl FormApp {
    fn blur_all(&mut self) {
        self.name.blur();
        self.email.blur();
        self.password.blur();
    }

    fn focus_current(&mut self) {
        match self.focused_field {
            0 => self.name.focus(),
            1 => self.email.focus(),
            2 => self.password.focus(),
            _ => {}
        }
    }

    fn validate(&mut self) -> bool {
        self.error = None;

        if self.name.value().trim().is_empty() {
            self.error = Some("Name is required".to_string());
            return false;
        }

        if self.email.value().trim().is_empty() {
            self.error = Some("Email is required".to_string());
            return false;
        }

        if !self.email.value().contains('@') {
            self.error = Some("Invalid email format".to_string());
            return false;
        }

        if self.password.value().len() < 6 {
            self.error = Some("Password must be at least 6 characters".to_string());
            return false;
        }

        true
    }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let app = FormApp {
        name: TextInput::new().with_prefix("> "),
        email: TextInput::new().with_prefix("> "),
        password: TextInput::new().with_prefix("> ").with_mask('*'),
        focused_field: 0,
        submitted: false,
        error: None,
    };

    ElementProgramBuilder::new(app)
        .with_alt_screen()
        .with_fps(30)
        .run()
        .await
}