use crate::models::Event;
use ratatui_macros::span;
use std::{
cmp::{max, min},
fmt::Display,
};
use ratatui::{
layout::{Alignment, Constraint, Rect},
style::{Color, Modifier, Style, Stylize},
text::{Line, Text},
widgets::{Block, BorderType, Borders, Cell, Clear, Padding, Row, Table, TableState},
};
use tui_textarea::Key;
use super::utils;
const ROW_HEIGHT: usize = 1;
pub struct HelpScreen<'a> {
showing: bool,
state: TableState,
rows: Vec<Row<'a>>,
keybindings: Vec<KeyBinding>,
last_known_width: usize,
last_know_height: usize,
}
impl HelpScreen<'_> {
pub fn showing(&self) -> bool {
self.showing
}
pub fn toggle_showing(&mut self) {
self.showing = !self.showing;
}
fn next_row(&mut self) {
let i = match self.state.selected() {
Some(i) => max(min(self.rows.len() as i32 - 1, i as i32 + 1), 0),
None => 0,
} as usize;
self.state.select(Some(i));
}
fn prev_row(&mut self) {
let i = match self.state.selected() {
Some(i) => max(0, (i as i32) - 1),
None => 0,
} as usize;
self.state.select(Some(i));
}
pub fn handle_key_event(&mut self, event: &Event) -> bool {
match event {
Event::KeyboardF1 => {
self.showing = !self.showing;
return false;
}
Event::Quit => {
self.showing = false;
return true;
}
Event::KeyboardCharInput(input) => match input.key {
Key::Char('j') => self.next_row(),
Key::Char('k') => self.prev_row(),
Key::Char('q') => {
self.showing = false;
return false;
}
_ => {}
},
Event::UiScrollDown => self.next_row(),
Event::UiScrollUp => self.prev_row(),
_ => {}
}
false
}
pub fn render(&mut self, frame: &mut ratatui::Frame, area: ratatui::layout::Rect) {
if !self.showing {
return;
}
let block = Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::LightBlue))
.padding(Padding::symmetric(1, 0))
.title(Line::from(" Help ").bold())
.title_alignment(Alignment::Center)
.title_bottom(Line::from(vec![
" ".into(),
span!("q").green().bold(),
span!(" to close, ").white(),
span!("↑/k/↓/j").green().bold(),
span!(" to move up/down ").white(),
]))
.style(Style::default());
frame.render_widget(Clear, area);
if self.last_known_width != area.width as usize
|| self.last_know_height != area.height as usize
{
self.last_known_width = area.width as usize;
self.last_know_height = area.height as usize;
self.rows = self.build_rows((area.width as f32 * 0.75).ceil() as usize);
let row_index = 0;
self.state.select(Some(row_index));
}
let selected_row_style = Style::default().add_modifier(Modifier::REVERSED);
let table = Table::new(
self.rows.clone(),
[Constraint::Percentage(25), Constraint::Percentage(75)],
)
.block(block)
.row_highlight_style(selected_row_style)
.cell_highlight_style(Style::default().bg(Color::White));
frame.render_stateful_widget(table, area, &mut self.state);
}
pub fn render_help_line(&self, frame: &mut ratatui::Frame, area: Rect) {
let mut instructions = self
.keybindings
.iter()
.filter(|b| !b.short_description.is_empty())
.flat_map(|b| {
let key = b.key().to_string();
let desc = b.short_description.clone();
vec![
span!(Style::default().fg(Color::LightGreen).add_modifier(Modifier::BOLD); key),
" ".into(),
span!(Style::default().fg(Color::White); desc),
" | ".into(),
]
})
.collect::<Vec<_>>();
instructions.pop();
let line = Line::from(instructions).light_green();
frame.render_widget(line, area);
}
fn build_rows<'b>(&self, max_width: usize) -> Vec<Row<'b>> {
let mut rows = vec![];
for binding in self.keybindings.iter() {
let key = Cell::from(binding.key().to_string()).style(Style::default());
let desc = utils::split_to_lines(binding.long_description().to_string(), max_width - 2);
rows.push(Row::new(vec![key, Cell::from(Text::from(desc))]).height(ROW_HEIGHT as u16));
}
rows
}
}
fn build_key_bindings() -> Vec<KeyBinding> {
vec![
KeyBinding::new(Input::new(Key::Char('q')), "Close Popup"),
KeyBinding::new(Input::new(Key::F(1)), "Show Help").with_short_desc("Help"),
KeyBinding::new(Input::new(Key::Char('h')).ctrl(), "Show Chat [H]istory")
.with_short_desc("History"),
KeyBinding::new(Input::new(Key::Char('q')).ctrl(), "[Q]uit").with_short_desc("Quit"),
KeyBinding::new(
Input::new(Key::Char('c')).ctrl(),
"Abort Request/[C]lear Chat",
),
KeyBinding::new(Input::new(Key::Char('r')).ctrl(), "[R]egenerate Response"),
KeyBinding::new(Input::new(Key::Char('l')).ctrl(), "[L]ist/Select Model"),
KeyBinding::new(Input::new(Key::Char('e')).ctrl(), "[E]dit Mode"),
KeyBinding::new(Input::new(Key::Char('n')).ctrl(), "[N]ew Chat"),
KeyBinding::new(Input::new(Key::Up), "Scroll Up"),
KeyBinding::new(Input::new(Key::Down), "Scroll Down"),
KeyBinding::new(Input::new(Key::Up).ctrl(), "Scroll Page Up"),
KeyBinding::new(Input::new(Key::Down).ctrl(), "Scroll Page Down"),
]
}
pub struct Input {
key: Key,
shift: bool,
ctrl: bool,
alt: bool,
}
impl Input {
pub fn new(key: Key) -> Self {
Self {
key,
shift: false,
ctrl: false,
alt: false,
}
}
pub fn ctrl(mut self) -> Self {
self.ctrl = true;
self
}
pub fn shift(mut self) -> Self {
self.shift = true;
self
}
pub fn alt(mut self) -> Self {
self.alt = true;
self
}
}
pub struct KeyBinding {
key: Input,
long_description: String,
short_description: String,
}
impl KeyBinding {
fn new(key: Input, description: &str) -> Self {
Self {
key,
long_description: description.to_string(),
short_description: String::new(),
}
}
fn with_short_desc(mut self, short_description: &str) -> Self {
self.short_description = short_description.to_string();
self
}
pub fn key(&self) -> &Input {
&self.key
}
pub fn long_description(&self) -> &str {
&self.long_description
}
pub fn short_description(&self) -> &str {
&self.short_description
}
}
impl Display for Input {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut modifer = String::new();
if self.ctrl {
modifer.push_str("Ctrl+");
}
if self.alt {
modifer.push_str("Alt+");
}
let key = match self.key {
Key::Char(c) => {
if self.shift {
c.to_uppercase().to_string()
} else {
c.to_string()
}
}
Key::F(n) => format!("F{}", n),
Key::Backspace => "Backspace".to_string(),
Key::Enter => "Enter".to_string(),
Key::Left => "Left".to_string(),
Key::Right => "Right".to_string(),
Key::Up => "Up".to_string(),
Key::Down => "Down".to_string(),
Key::Tab => "Tab".to_string(),
Key::Delete => "Delete".to_string(),
Key::Home => "Home".to_string(),
Key::End => "End".to_string(),
Key::PageUp => "PageUp".to_string(),
Key::PageDown => "PageDown".to_string(),
Key::Esc => "Esc".to_string(),
_ => "Unknown".to_string(),
};
write!(f, "{}{}", modifer, key)
}
}
impl Default for HelpScreen<'_> {
fn default() -> Self {
Self {
showing: false,
state: TableState::default().with_selected(0),
rows: vec![],
last_known_width: 0,
last_know_height: 0,
keybindings: build_key_bindings(),
}
}
}