Skip to main content

chatty_rs/app/ui/
help.rs

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    /// handle_key_event handles key events for the help menu.
59    /// Returns true when user hit quit (Ctrl + Q).
60    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(); // remove the last " | "
154
155        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}