tui_commander/
ui.rs

1use ratatui::crossterm::event::Event;
2use ratatui::crossterm::event::KeyEvent;
3use ratatui::layout::Constraint;
4use ratatui::layout::Layout;
5use ratatui::layout::Rect;
6use ratatui::style::Modifier;
7use ratatui::style::Style;
8use ratatui::style::Stylize;
9use ratatui::text::Line;
10use ratatui::text::Span;
11use ratatui::text::Text;
12use ratatui::widgets::Block;
13use ratatui::widgets::Borders;
14use ratatui::widgets::List;
15use ratatui::widgets::ListDirection;
16use ratatui::widgets::Paragraph;
17use ratatui::widgets::StatefulWidget;
18use ratatui::widgets::Widget;
19use tui_input::backend::crossterm::EventHandler;
20use tui_input::Input;
21
22pub struct Ui<Context> {
23    input: Input,
24
25    pub max_height_percent: u16,
26    _pd: std::marker::PhantomData<fn(Context)>,
27}
28
29impl<Context> Default for Ui<Context> {
30    fn default() -> Self {
31        Self {
32            input: Input::default(),
33            max_height_percent: 50,
34            _pd: std::marker::PhantomData,
35        }
36    }
37}
38
39impl<Context> Ui<Context> {
40    pub fn handle_key_press(&mut self, event: KeyEvent) {
41        self.input.handle_event(&Event::Key(event));
42    }
43
44    pub fn value(&self) -> &str {
45        self.input.value()
46    }
47
48    #[inline]
49    pub fn reset(&mut self) {
50        self.input.reset()
51    }
52}
53
54impl<Context> StatefulWidget for &mut Ui<Context>
55where
56    Context: crate::Context + Sized,
57{
58    type State = crate::Commander<Context>;
59
60    fn render(self, area: Rect, buf: &mut ratatui::prelude::Buffer, state: &mut Self::State)
61    where
62        Self: Sized,
63    {
64        let msg = vec![
65            Span::styled("ESC", Style::default().add_modifier(Modifier::BOLD)),
66            Span::raw(" to exit prompt"),
67        ];
68        let style = Style::default().add_modifier(Modifier::RAPID_BLINK);
69
70        let suggestions = state.suggestions();
71
72        let suggestions_height = self.max_height_percent.min({
73            suggestions.len() as u16 + 2 // borders
74        });
75
76        let [_rest, commander_area] = Layout::vertical([
77            Constraint::Fill(1),
78            Constraint::Length(suggestions_height + 3),
79        ])
80        .flex(ratatui::layout::Flex::Start)
81        .areas(area);
82
83        let [suggestions_area, command_area] = Layout::vertical([
84            Constraint::Length(suggestions_height),
85            Constraint::Length(3),
86        ])
87        .areas(commander_area);
88
89        if !suggestions.is_empty() {
90            ratatui::widgets::Clear.render(suggestions_area, buf);
91            let list = List::new(suggestions.clone())
92                .block(Block::bordered().title("Suggestions"))
93                .style(Style::new().white())
94                .highlight_style(Style::new().italic())
95                .highlight_symbol(">>")
96                .repeat_highlight_symbol(true)
97                .direction(ListDirection::BottomToTop);
98            Widget::render(list, suggestions_area, buf)
99        }
100
101        let [commander_logo_area, inserting_area, desc_area] = Layout::horizontal([
102            Constraint::Min(3),
103            Constraint::Percentage(100),
104            Constraint::Min(20),
105        ])
106        .areas(command_area);
107
108        let logo = Paragraph::new(
109            Text::from(Line::from(Span::styled(
110                ":",
111                Style::default().add_modifier(Modifier::BOLD),
112            )))
113            .style(style),
114        )
115        .block(Block::default().borders(Borders::ALL));
116
117        let desc_text = Paragraph::new(Text::from(Line::from(msg)).style(style))
118            .block(Block::default().borders(Borders::ALL));
119
120        let input = Paragraph::new(self.input.value())
121            .style(
122                if state.is_unknown_command() || !state.current_args_are_valid().unwrap_or(true) {
123                    Style::default().on_red()
124                } else {
125                    Style::default()
126                },
127            )
128            .block(Block::default().borders(Borders::ALL).title("Input"));
129
130        ratatui::widgets::Clear.render(command_area, buf);
131        logo.render(commander_logo_area, buf);
132        input.render(inserting_area, buf);
133        desc_text.render(desc_area, buf);
134    }
135}