tui_components/
lib.rs

1pub mod components;
2pub mod rect_ext;
3
4use std::fmt::Display;
5use std::io::{stdout, Stdout};
6use std::time::Duration;
7
8use crossterm::event::{poll, read, Event as TermEvent, KeyEvent, MouseEvent};
9use crossterm::execute;
10use crossterm::terminal::{disable_raw_mode, enable_raw_mode, SetTitle};
11use crossterm::ErrorKind;
12use tui::backend::CrosstermBackend;
13use tui::buffer::Buffer;
14use tui::layout::Rect;
15use tui::text::Spans;
16use tui::widgets::Widget;
17use tui::Terminal;
18
19pub use crossterm;
20pub use tui;
21
22pub struct Wrapper<'a, A: App>(pub &'a mut A);
23
24impl<'a, A: App> Widget for Wrapper<'a, A> {
25    fn render(self, area: Rect, buf: &mut Buffer) {
26        self.0.draw(area, buf)
27    }
28}
29
30/// A trait enabling a nested layout of UI components
31pub trait Component {
32    type Response;
33    type DrawResponse;
34
35    fn handle_event(&mut self, event: Event) -> Self::Response;
36
37    fn draw(&mut self, rect: Rect, buffer: &mut Buffer) -> Self::DrawResponse;
38}
39
40// A trait representing a top-level component
41pub trait App {
42    fn handle_event(&mut self, event: Event) -> AppResponse;
43
44    fn draw(&mut self, rect: Rect, buffer: &mut Buffer);
45}
46
47/// A trait for components that can be rendered as spans
48pub trait Spannable {
49    fn get_spans<'a, 'b>(&'a self) -> Spans<'b>;
50}
51
52#[derive(Debug, Copy, Clone)]
53pub enum Event {
54    Key(KeyEvent),
55    Mouse(MouseEvent),
56}
57
58pub enum AppResponse {
59    Exit,
60    None,
61}
62
63pub fn run<A: App>(app: &mut A, title: Option<String>) -> Result<(), ErrorKind> {
64    let mut should_refresh = true;
65
66    let mut t = setup_terminal(title)?;
67
68    loop {
69        if should_refresh {
70            t.draw(|f| {
71                let size = f.size();
72                f.render_widget(Wrapper(app), size);
73            })
74            .unwrap();
75            should_refresh = false;
76        }
77
78        if poll(Duration::from_secs_f64(1.0 / 60.0)).unwrap() {
79            should_refresh = true;
80            let event = read().unwrap();
81            let comp_event = match event {
82                TermEvent::Resize(..) => continue,
83                TermEvent::Mouse(m) => Event::Mouse(m),
84                TermEvent::Key(k) => Event::Key(k),
85            };
86            match app.handle_event(comp_event) {
87                AppResponse::Exit => break,
88                AppResponse::None => {}
89            }
90        }
91    }
92
93    close_terminal(&mut t)?;
94    Ok(())
95}
96
97pub fn set_title<S: Display>(title: &S) -> Result<(), ErrorKind> {
98    execute!(stdout(), SetTitle(title))
99}
100
101fn setup_terminal(title: Option<String>) -> Result<Terminal<CrosstermBackend<Stdout>>, ErrorKind> {
102    if let Some(title) = title {
103        set_title(&title)?;
104    }
105
106    enable_raw_mode()?;
107    let mut t = Terminal::new(CrosstermBackend::new(stdout())).unwrap();
108    t.clear().unwrap();
109    Ok(t)
110}
111
112fn close_terminal(_t: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<(), ErrorKind> {
113    disable_raw_mode()?;
114    Ok(())
115}