use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Position, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Clear, Paragraph},
};
use crate::app::{App, InputMode, ModalState};
pub(super) fn render_help(frame: &mut Frame) {
let area = centered_rect(frame.area(), 50, 80);
frame.render_widget(Clear, area);
let help_lines = vec![
Line::styled(
"─── Keyboard Shortcuts ───",
Style::default().fg(Color::Cyan).bold(),
),
Line::raw(""),
Line::styled(" Navigation", Style::default().bold()),
Line::from(vec![
Span::styled(" j / ↓ ", Style::default().fg(Color::Yellow)),
Span::raw("Move down"),
]),
Line::from(vec![
Span::styled(" k / ↑ ", Style::default().fg(Color::Yellow)),
Span::raw("Move up"),
]),
Line::from(vec![
Span::styled(" g / Home ", Style::default().fg(Color::Yellow)),
Span::raw("Go to top"),
]),
Line::from(vec![
Span::styled(" G / End ", Style::default().fg(Color::Yellow)),
Span::raw("Go to bottom"),
]),
Line::from(vec![
Span::styled(" Ctrl+d ", Style::default().fg(Color::Yellow)),
Span::raw("Page down"),
]),
Line::from(vec![
Span::styled(" Ctrl+u ", Style::default().fg(Color::Yellow)),
Span::raw("Page up"),
]),
Line::raw(""),
Line::styled(" jj Commands", Style::default().bold()),
Line::from(vec![
Span::styled(" n ", Style::default().fg(Color::Yellow)),
Span::raw("New change"),
]),
Line::from(vec![
Span::styled(" N ", Style::default().fg(Color::Yellow)),
Span::raw("New change with message"),
]),
Line::from(vec![
Span::styled(" e ", Style::default().fg(Color::Yellow)),
Span::raw("Edit revision"),
]),
Line::from(vec![
Span::styled(" d ", Style::default().fg(Color::Yellow)),
Span::raw("Describe revision"),
]),
Line::from(vec![
Span::styled(" b ", Style::default().fg(Color::Yellow)),
Span::raw("Set bookmark"),
]),
Line::from(vec![
Span::styled(" a ", Style::default().fg(Color::Yellow)),
Span::raw("Abandon revision"),
]),
Line::from(vec![
Span::styled(" s ", Style::default().fg(Color::Yellow)),
Span::raw("Squash into parent"),
]),
Line::from(vec![
Span::styled(" f ", Style::default().fg(Color::Yellow)),
Span::raw("Git fetch"),
]),
Line::from(vec![
Span::styled(" p ", Style::default().fg(Color::Yellow)),
Span::raw("Git push"),
]),
Line::from(vec![
Span::styled(" u ", Style::default().fg(Color::Yellow)),
Span::raw("Undo last operation"),
]),
Line::from(vec![
Span::styled(" r ", Style::default().fg(Color::Yellow)),
Span::raw("Rebase to destination"),
]),
Line::raw(""),
Line::styled(" Detail View", Style::default().bold()),
Line::from(vec![
Span::styled(" d ", Style::default().fg(Color::Yellow)),
Span::raw("View file diffs"),
]),
Line::raw(""),
Line::styled(" Diff View", Style::default().bold()),
Line::from(vec![
Span::styled(" j / ↓ ", Style::default().fg(Color::Yellow)),
Span::raw("Select next file"),
]),
Line::from(vec![
Span::styled(" k / ↑ ", Style::default().fg(Color::Yellow)),
Span::raw("Select previous file"),
]),
Line::from(vec![
Span::styled(" Ctrl+d/u ", Style::default().fg(Color::Yellow)),
Span::raw("Scroll diff vertically"),
]),
Line::from(vec![
Span::styled(" ← / → ", Style::default().fg(Color::Yellow)),
Span::raw("Scroll diff horizontally"),
]),
Line::from(vec![
Span::styled(" q / Esc ", Style::default().fg(Color::Yellow)),
Span::raw("Back to detail"),
]),
Line::raw(""),
Line::styled(" General", Style::default().bold()),
Line::from(vec![
Span::styled(" Enter ", Style::default().fg(Color::Yellow)),
Span::raw("Open detail view"),
]),
Line::from(vec![
Span::styled(" q ", Style::default().fg(Color::Yellow)),
Span::raw("Quit / Close view"),
]),
Line::from(vec![
Span::styled(" Esc ", Style::default().fg(Color::Yellow)),
Span::raw("Close detail / help"),
]),
Line::from(vec![
Span::styled(" ? ", Style::default().fg(Color::Yellow)),
Span::raw("Toggle this help"),
]),
];
let help_widget = Paragraph::new(help_lines).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.title(" Help "),
);
frame.render_widget(help_widget, area);
}
fn centered_rect(area: Rect, percent_x: u16, percent_y: u16) -> Rect {
let vertical = Layout::vertical([Constraint::Percentage(percent_y)]).flex(Flex::Center);
let horizontal = Layout::horizontal([Constraint::Percentage(percent_x)]).flex(Flex::Center);
let [area] = vertical.areas(area);
let [area] = horizontal.areas(area);
area
}
pub(super) fn render_modal_overlay(frame: &mut Frame, app: &App) {
let ModalState::Confirm(action) = &app.modal else {
return;
};
let message = action.confirm_message();
let area = frame.area();
let width = (message.len() as u16 + 6)
.max(30)
.min(area.width.saturating_sub(4));
let height = 5.min(area.height);
let x = (area.width.saturating_sub(width)) / 2;
let y = (area.height.saturating_sub(height)) / 2;
let modal_area = Rect::new(x, y, width, height);
frame.render_widget(Clear, modal_area);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Yellow))
.title(" Confirm ")
.title_style(Style::default().fg(Color::Yellow).bold());
let inner_area = block.inner(modal_area);
frame.render_widget(block, modal_area);
let chunks = Layout::vertical([
Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ])
.split(inner_area);
let message_paragraph = Paragraph::new(message).alignment(ratatui::layout::Alignment::Center);
frame.render_widget(message_paragraph, chunks[0]);
let buttons = Line::from(vec![
Span::styled(" [Y]es ", Style::default().fg(Color::Green).bold()),
Span::raw(" "),
Span::styled(" [N]o ", Style::default().fg(Color::Red).bold()),
]);
let buttons_paragraph = Paragraph::new(buttons).alignment(ratatui::layout::Alignment::Center);
frame.render_widget(buttons_paragraph, chunks[2]);
}
pub(super) fn render_input_overlay(frame: &mut Frame, app: &App) {
let Some(mode) = &app.input_mode else {
return;
};
let area = frame.area();
let width = (area.width.saturating_mul(60) / 100)
.max(40)
.min(area.width.saturating_sub(4));
let height = 3.min(area.height);
let x = (area.width.saturating_sub(width)) / 2;
let y = (area.height.saturating_sub(height)) / 2;
let input_area = Rect::new(x, y, width, height);
frame.render_widget(Clear, input_area);
let title = match mode {
InputMode::Describe => " Describe ",
InputMode::BookmarkSet => " Set Bookmark ",
InputMode::NewWithMessage => " New Change ",
InputMode::RebaseDestination => " Rebase to ",
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.title(title)
.title_style(Style::default().fg(Color::Cyan).bold());
let inner_area = block.inner(input_area);
frame.render_widget(block, input_area);
let input_value = app.input.value();
let display_text = if input_value.is_empty() {
Span::styled(mode.placeholder(), Style::default().fg(Color::DarkGray))
} else {
Span::raw(input_value)
};
let scroll = app.input.visual_scroll(inner_area.width as usize);
let input_paragraph = Paragraph::new(Line::from(display_text)).scroll((0, scroll as u16));
frame.render_widget(input_paragraph, inner_area);
if !input_value.is_empty() || app.is_input_mode() {
let cursor_x = app.input.visual_cursor().saturating_sub(scroll);
let cursor_x = (cursor_x as u16).min(inner_area.width.saturating_sub(1));
frame.set_cursor_position(Position::new(
inner_area.x.saturating_add(cursor_x),
inner_area.y,
));
}
}