1use crate::models::Event;
2use ratatui_macros::span;
3use std::{
4 cmp::{max, min},
5 fmt::Display,
6};
7
8use ratatui::{
9 layout::{Alignment, Constraint, Rect},
10 style::{Color, Modifier, Style, Stylize},
11 text::{Line, Text},
12 widgets::{Block, BorderType, Borders, Cell, Clear, Padding, Row, Table, TableState},
13};
14use tui_textarea::Key;
15
16use super::utils;
17
18const ROW_HEIGHT: usize = 1;
19
20pub struct HelpScreen<'a> {
21 showing: bool,
22
23 state: TableState,
24
25 rows: Vec<Row<'a>>,
26 keybindings: Vec<KeyBinding>,
27 last_known_width: usize,
28 last_know_height: usize,
29}
30
31impl HelpScreen<'_> {
32 pub fn showing(&self) -> bool {
33 self.showing
34 }
35
36 pub fn toggle_showing(&mut self) {
37 self.showing = !self.showing;
38 }
39
40 fn next_row(&mut self) {
41 let i = match self.state.selected() {
42 Some(i) => max(min(self.rows.len() as i32 - 1, i as i32 + 1), 0),
43 None => 0,
44 } as usize;
45
46 self.state.select(Some(i));
47 }
48
49 fn prev_row(&mut self) {
50 let i = match self.state.selected() {
51 Some(i) => max(0, (i as i32) - 1),
52 None => 0,
53 } as usize;
54
55 self.state.select(Some(i));
56 }
57
58 pub fn handle_key_event(&mut self, event: &Event) -> bool {
61 match event {
62 Event::KeyboardF1 => {
63 self.showing = !self.showing;
64 return false;
65 }
66
67 Event::Quit => {
68 self.showing = false;
69 return true;
70 }
71
72 Event::KeyboardCharInput(input) => match input.key {
73 Key::Char('j') => self.next_row(),
74 Key::Char('k') => self.prev_row(),
75 Key::Char('q') => {
76 self.showing = false;
77 return false;
78 }
79 _ => {}
80 },
81
82 Event::UiScrollDown => self.next_row(),
83 Event::UiScrollUp => self.prev_row(),
84
85 _ => {}
86 }
87
88 false
89 }
90
91 pub fn render(&mut self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
92 if !self.showing {
93 return;
94 }
95
96 let block = Block::default()
97 .borders(Borders::ALL)
98 .border_type(BorderType::Rounded)
99 .border_style(Style::default().fg(Color::LightBlue))
100 .padding(Padding::symmetric(1, 0))
101 .title(Line::from(" Help ").bold())
102 .title_alignment(Alignment::Center)
103 .title_bottom(Line::from(vec![
104 " ".into(),
105 span!("q").green().bold(),
106 span!(" to close, ").white(),
107 span!("↑/k/↓/j").green().bold(),
108 span!(" to move up/down ").white(),
109 ]))
110 .style(Style::default());
111 frame.render_widget(Clear, area);
112
113 if self.last_known_width != area.width as usize
114 || self.last_know_height != area.height as usize
115 {
116 self.last_known_width = area.width as usize;
117 self.last_know_height = area.height as usize;
118
119 self.rows = self.build_rows((area.width as f32 * 0.75).ceil() as usize);
120 let row_index = 0;
121 self.state.select(Some(row_index));
122 }
123
124 let selected_row_style = Style::default().add_modifier(Modifier::REVERSED);
125
126 let table = Table::new(
127 self.rows.clone(),
128 [Constraint::Percentage(25), Constraint::Percentage(75)],
129 )
130 .block(block)
131 .row_highlight_style(selected_row_style)
132 .cell_highlight_style(Style::default().bg(Color::White));
133
134 frame.render_stateful_widget(table, area, &mut self.state);
135 }
136
137 pub fn render_help_line(&self, frame: &mut ratatui::Frame, area: Rect) {
138 let mut instructions = self
139 .keybindings
140 .iter()
141 .filter(|b| !b.short_description.is_empty())
142 .flat_map(|b| {
143 let key = b.key().to_string();
144 let desc = b.short_description.clone();
145 vec![
146 span!(Style::default().fg(Color::LightGreen).add_modifier(Modifier::BOLD); key),
147 " ".into(),
148 span!(Style::default().fg(Color::White); desc),
149 " | ".into(),
150 ]
151 })
152 .collect::<Vec<_>>();
153 instructions.pop(); let line = Line::from(instructions).light_green();
156 frame.render_widget(line, area);
157 }
158
159 fn build_rows<'b>(&self, max_width: usize) -> Vec<Row<'b>> {
160 let mut rows = vec![];
161 for binding in self.keybindings.iter() {
162 let key = Cell::from(binding.key().to_string()).style(Style::default());
163 let desc = utils::split_to_lines(binding.long_description().to_string(), max_width - 2);
164 rows.push(Row::new(vec![key, Cell::from(Text::from(desc))]).height(ROW_HEIGHT as u16));
165 }
166 rows
167 }
168}
169
170fn build_key_bindings() -> Vec<KeyBinding> {
171 vec![
172 KeyBinding::new(Input::new(Key::Char('q')), "Close Popup"),
173 KeyBinding::new(Input::new(Key::F(1)), "Show Help").with_short_desc("Help"),
174 KeyBinding::new(Input::new(Key::Char('h')).ctrl(), "Show Chat [H]istory")
175 .with_short_desc("History"),
176 KeyBinding::new(Input::new(Key::Char('q')).ctrl(), "[Q]uit").with_short_desc("Quit"),
177 KeyBinding::new(
178 Input::new(Key::Char('c')).ctrl(),
179 "Abort Request/[C]lear Chat",
180 ),
181 KeyBinding::new(Input::new(Key::Char('r')).ctrl(), "[R]egenerate Response"),
182 KeyBinding::new(Input::new(Key::Char('l')).ctrl(), "[L]ist/Select Model"),
183 KeyBinding::new(Input::new(Key::Char('e')).ctrl(), "[E]dit Mode"),
184 KeyBinding::new(Input::new(Key::Char('n')).ctrl(), "[N]ew Chat"),
185 KeyBinding::new(Input::new(Key::Up), "Scroll Up"),
186 KeyBinding::new(Input::new(Key::Down), "Scroll Down"),
187 KeyBinding::new(Input::new(Key::Up).ctrl(), "Scroll Page Up"),
188 KeyBinding::new(Input::new(Key::Down).ctrl(), "Scroll Page Down"),
189 ]
190}
191
192pub struct Input {
193 key: Key,
194 shift: bool,
195 ctrl: bool,
196 alt: bool,
197}
198
199impl Input {
200 pub fn new(key: Key) -> Self {
201 Self {
202 key,
203 shift: false,
204 ctrl: false,
205 alt: false,
206 }
207 }
208
209 pub fn ctrl(mut self) -> Self {
210 self.ctrl = true;
211 self
212 }
213
214 pub fn shift(mut self) -> Self {
215 self.shift = true;
216 self
217 }
218
219 pub fn alt(mut self) -> Self {
220 self.alt = true;
221 self
222 }
223}
224
225pub struct KeyBinding {
226 key: Input,
227 long_description: String,
228 short_description: String,
229}
230
231impl KeyBinding {
232 fn new(key: Input, description: &str) -> Self {
233 Self {
234 key,
235 long_description: description.to_string(),
236 short_description: String::new(),
237 }
238 }
239
240 fn with_short_desc(mut self, short_description: &str) -> Self {
241 self.short_description = short_description.to_string();
242 self
243 }
244
245 pub fn key(&self) -> &Input {
246 &self.key
247 }
248
249 pub fn long_description(&self) -> &str {
250 &self.long_description
251 }
252
253 pub fn short_description(&self) -> &str {
254 &self.short_description
255 }
256}
257
258impl Display for Input {
259 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260 let mut modifer = String::new();
261
262 if self.ctrl {
263 modifer.push_str("Ctrl+");
264 }
265
266 if self.alt {
267 modifer.push_str("Alt+");
268 }
269
270 let key = match self.key {
271 Key::Char(c) => {
272 if self.shift {
273 c.to_uppercase().to_string()
274 } else {
275 c.to_string()
276 }
277 }
278 Key::F(n) => format!("F{}", n),
279 Key::Backspace => "Backspace".to_string(),
280 Key::Enter => "Enter".to_string(),
281 Key::Left => "Left".to_string(),
282 Key::Right => "Right".to_string(),
283 Key::Up => "Up".to_string(),
284 Key::Down => "Down".to_string(),
285 Key::Tab => "Tab".to_string(),
286 Key::Delete => "Delete".to_string(),
287 Key::Home => "Home".to_string(),
288 Key::End => "End".to_string(),
289 Key::PageUp => "PageUp".to_string(),
290 Key::PageDown => "PageDown".to_string(),
291 Key::Esc => "Esc".to_string(),
292 _ => "Unknown".to_string(),
293 };
294
295 write!(f, "{}{}", modifer, key)
296 }
297}
298
299impl Default for HelpScreen<'_> {
300 fn default() -> Self {
301 Self {
302 showing: false,
303 state: TableState::default().with_selected(0),
304 rows: vec![],
305 last_known_width: 0,
306 last_know_height: 0,
307 keybindings: build_key_bindings(),
308 }
309 }
310}