aether-tui 0.2.0

A lightweight terminal UI rendering library for building rich CLI applications
Documentation
use std::time::Duration;
#[allow(clippy::wildcard_imports)]
use tui::*;

#[derive(Debug, Clone)]
struct Language {
    name: String,
    ext: String,
}

impl Searchable for Language {
    fn search_text(&self) -> String {
        format!("{} {}", self.name, self.ext)
    }
}

struct ComboboxStory {
    combobox: Combobox<Language>,
}

impl Component for ComboboxStory {
    type Message = ();

    async fn on_event(&mut self, event: &Event) -> Option<Vec<()>> {
        self.combobox.handle_picker_event(event).map(|_| vec![])
    }

    fn render(&mut self, ctx: &ViewContext) -> Frame {
        let lines = self.combobox.render_items(ctx, |lang, selected, inner| {
            let text = format!("{} (.{})", lang.name, lang.ext);
            if selected {
                let mut line = Line::with_style(text, inner.theme.selected_row_style());
                line.extend_bg_to_width(inner.size.width as usize);
                line
            } else {
                Line::new(text)
            }
        });
        Frame::new(lines)
    }
}

enum TuiStory {
    TextField(TextField),
    Checkbox(Checkbox),
    Spinner(Spinner),
    NumberField(NumberField),
    RadioSelect(RadioSelect),
    MultiSelect(MultiSelect),
    Combobox(ComboboxStory),
}

impl Component for TuiStory {
    type Message = ();

    async fn on_event(&mut self, event: &Event) -> Option<Vec<()>> {
        match self {
            Self::TextField(c) => c.on_event(event).await.map(|_| vec![]),
            Self::Checkbox(c) => c.on_event(event).await.map(|_| vec![]),
            Self::Spinner(c) => c.on_event(event).await,
            Self::NumberField(c) => c.on_event(event).await.map(|_| vec![]),
            Self::RadioSelect(c) => c.on_event(event).await.map(|_| vec![]),
            Self::MultiSelect(c) => c.on_event(event).await.map(|_| vec![]),
            Self::Combobox(c) => c.on_event(event).await,
        }
    }

    fn render(&mut self, ctx: &ViewContext) -> Frame {
        match self {
            Self::TextField(c) => c.render(ctx),
            Self::Checkbox(c) => c.render(ctx),
            Self::Spinner(c) => c.render(ctx),
            Self::NumberField(c) => c.render(ctx),
            Self::RadioSelect(c) => c.render(ctx),
            Self::MultiSelect(c) => c.render(ctx),
            Self::Combobox(c) => c.render(ctx),
        }
    }
}

fn sample_options() -> Vec<SelectOption> {
    vec![
        SelectOption { value: "rust".into(), title: "Rust".into(), description: Some("Systems programming".into()) },
        SelectOption { value: "go".into(), title: "Go".into(), description: Some("Cloud infrastructure".into()) },
        SelectOption { value: "python".into(), title: "Python".into(), description: Some("Data science & ML".into()) },
    ]
}

fn sample_languages() -> Vec<Language> {
    vec![
        Language { name: "Rust".into(), ext: "rs".into() },
        Language { name: "Python".into(), ext: "py".into() },
        Language { name: "TypeScript".into(), ext: "ts".into() },
        Language { name: "Go".into(), ext: "go".into() },
        Language { name: "Ruby".into(), ext: "rb".into() },
        Language { name: "Java".into(), ext: "java".into() },
        Language { name: "C++".into(), ext: "cpp".into() },
        Language { name: "Haskell".into(), ext: "hs".into() },
    ]
}

fn stories() -> Vec<(String, TuiStory)> {
    vec![
        ("TextField".into(), TuiStory::TextField(TextField::new("Hello, gallery!".into()))),
        ("Checkbox".into(), TuiStory::Checkbox(Checkbox::new(false))),
        (
            "Spinner".into(),
            TuiStory::Spinner({
                let mut s = Spinner::braille();
                s.visible = true;
                s
            }),
        ),
        ("NumberField".into(), TuiStory::NumberField(NumberField::new(String::new(), false))),
        ("RadioSelect".into(), TuiStory::RadioSelect(RadioSelect::new(sample_options(), 0))),
        ("MultiSelect".into(), TuiStory::MultiSelect(MultiSelect::new(sample_options(), vec![false, false, false]))),
        ("Combobox".into(), TuiStory::Combobox(ComboboxStory { combobox: Combobox::new(sample_languages()) })),
    ]
}

#[tokio::main]
async fn main() {
    let mut gallery = Gallery::new(stories());
    let size = terminal_size().unwrap_or((80, 24));
    let mut terminal = TerminalRuntime::new(
        std::io::stdout(),
        Theme::default(),
        size,
        TerminalConfig { bracketed_paste: true, mouse_capture: MouseCapture::Disabled },
    )
    .unwrap();
    let mut tick = tokio::time::interval(Duration::from_millis(100));
    tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);

    terminal.render_frame(|ctx| gallery.render(ctx)).unwrap();

    loop {
        tokio::select! {
            Some(raw) = terminal.next_event() => {
                if let CrosstermEvent::Resize(cols, rows) = &raw {
                    terminal.on_resize((*cols, *rows));
                }
                if let Ok(event) = Event::try_from(raw) {
                    if let Some(msgs) = gallery.on_event(&event).await
                        && msgs.iter().any(|m| matches!(m, GalleryMessage::Quit))
                    {
                        return;
                    }
                    terminal.render_frame(|ctx| gallery.render(ctx)).unwrap();
                }
            }
            _ = tick.tick() => {
                gallery.on_event(&Event::Tick).await;
                terminal.render_frame(|ctx| gallery.render(ctx)).unwrap();
            }
        }
    }
}