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 });
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}