mod chrome;
mod diff;
mod sidebar;
use crate::app::{App, Mode};
use crate::field::Input;
use crate::theme;
use ratatui::{
Frame,
layout::{Constraint, Flex, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span, Text},
widgets::{Block, BorderType, Clear, Padding, Paragraph},
};
pub fn draw(f: &mut Frame, app: &App) {
let [header, body, footer] = Layout::vertical([
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.areas(f.area());
let [sidebar, diffpane] =
Layout::horizontal([Constraint::Percentage(20), Constraint::Percentage(80)]).areas(body);
chrome::header(f, header, app);
sidebar::render(f, sidebar, app);
diff::render(f, diffpane, app);
chrome::footer(f, footer, app);
match &app.mode {
Mode::Commit(input) => input_popup(f, "commit message", input),
Mode::Branch(input) => input_popup(f, "switch or create branch", input),
Mode::Confirm => confirm_popup(f),
Mode::Help => help_popup(f),
Mode::Normal => {}
}
}
fn help_popup(f: &mut Frame) {
let area = popup(f.area(), 64, 16);
let block = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(theme::ACCENT))
.title(Line::styled(
" keybindings ",
Style::new().fg(theme::ACCENT),
))
.padding(Padding::new(2, 2, 1, 1))
.style(Style::new().bg(theme::BASE));
let inner = block.inner(area);
f.render_widget(Clear, area);
f.render_widget(block, area);
let [left, right] =
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]).areas(inner);
f.render_widget(Paragraph::new(help_left()), left);
f.render_widget(Paragraph::new(help_right()), right);
}
fn help_left() -> Text<'static> {
Text::from(vec![
help_header("navigate"),
help_entry("j / k", "move up / down"),
help_entry("h / l", "fold / unfold"),
help_entry("enter", "open / fold"),
help_entry("tab", "switch pane"),
Line::raw(""),
help_header("remote"),
help_entry("p", "pull"),
help_entry("P", "push"),
])
}
fn help_right() -> Text<'static> {
Text::from(vec![
help_header("stage & commit"),
help_entry("space", "stage / unstage"),
help_entry("c", "commit"),
help_entry("X", "discard all"),
Line::raw(""),
help_header("branch"),
help_entry("b", "switch / create"),
Line::raw(""),
help_header("general"),
help_entry("q", "quit"),
help_entry("? / esc", "close"),
])
}
fn help_header(text: &str) -> Line<'static> {
Line::styled(
text.to_string(),
Style::new().fg(theme::ACCENT).add_modifier(Modifier::BOLD),
)
}
fn help_entry(key: &str, label: &str) -> Line<'static> {
Line::from(vec![
Span::styled(
format!(" {key:>7} "),
Style::new().fg(theme::TEXT).bg(theme::SURFACE),
),
Span::styled(format!(" {label}"), Style::new().fg(theme::MUTED)),
])
}
pub(super) fn border_style(active: bool) -> Style {
if active {
Style::new().fg(theme::BORDER_FOCUS)
} else {
Style::new().fg(theme::BORDER)
}
}
pub(super) fn panel(title: &str, active: bool) -> Block<'static> {
let title_color = if active { theme::ACCENT } else { theme::MUTED };
Block::bordered()
.border_type(BorderType::Rounded)
.border_style(border_style(active))
.title(Line::styled(
format!(" {title} "),
Style::new().fg(title_color),
))
.style(Style::new().bg(theme::BASE))
}
fn input_popup(f: &mut Frame, title: &str, input: &Input) {
let area = popup(f.area(), 60, 3);
let block = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(theme::ACCENT))
.title(Line::styled(
format!(" {title} "),
Style::new().fg(theme::ACCENT),
))
.style(Style::new().bg(theme::BASE));
let inner = block.inner(area);
f.render_widget(Clear, area);
f.render_widget(block, area);
f.render_widget(
Paragraph::new(input.value.as_str()).style(Style::new().fg(theme::TEXT)),
inner,
);
f.set_cursor_position((inner.x + input.cursor_col(), inner.y));
}
fn confirm_popup(f: &mut Frame) {
let area = popup(f.area(), 60, 5);
let block = Block::bordered()
.border_type(BorderType::Rounded)
.border_style(Style::new().fg(theme::RED))
.title(Line::styled(
" discard everything? ",
Style::new().fg(theme::RED),
))
.style(Style::new().bg(theme::BASE));
let inner = block.inner(area);
f.render_widget(Clear, area);
f.render_widget(block, area);
let body = Text::from(vec![
Line::styled(
"git reset --hard && git clean -fd",
Style::new().fg(theme::MUTED),
),
Line::raw(""),
Line::styled("[y] yes [n] no", Style::new().fg(theme::YELLOW)),
]);
f.render_widget(Paragraph::new(body), inner);
}
fn popup(area: Rect, percent_width: u16, height: u16) -> Rect {
let [centered] = Layout::horizontal([Constraint::Percentage(percent_width)])
.flex(Flex::Center)
.areas(area);
let [centered] = Layout::vertical([Constraint::Length(height)])
.flex(Flex::Center)
.areas(centered);
centered
}