use super::*;
impl App {
pub(super) fn clear_expired_messages(&mut self) -> bool {
let had_error = self.error_message.is_some();
let had_success = self.success_message.is_some();
if let Some((_, when)) = &self.error_message
&& when.elapsed().as_secs() >= 5
{
self.error_message = None;
}
if let Some((_, when)) = &self.success_message
&& when.elapsed().as_secs() >= 3
{
self.success_message = None;
}
had_error != self.error_message.is_some() || had_success != self.success_message.is_some()
}
pub(super) fn draw(&mut self, frame: &mut ratatui::Frame) -> Result<()> {
let area = frame.area();
let outer = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(3), Constraint::Length(1)])
.split(area);
let main_area = outer[0];
let status_area = outer[1];
self.horizontal_layout = main_area.width >= 100;
let (repo_area, changes_area, graph_area) = if self.horizontal_layout {
let w = main_area.width as f64;
let c1 = (self.border_frac[0] * w).round() as u16;
let c2 = ((self.border_frac[1] - self.border_frac[0]) * w).round() as u16;
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(c1),
Constraint::Length(c2),
Constraint::Min(8),
])
.split(main_area);
(chunks[0], chunks[1], chunks[2])
} else {
let h = main_area.height as f64;
let r1 = (self.border_frac[0] * h).round() as u16;
let r2 = ((self.border_frac[1] - self.border_frac[0]) * h).round() as u16;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(r1),
Constraint::Length(r2),
Constraint::Min(3),
])
.split(main_area);
(chunks[0], chunks[1], chunks[2])
};
self.repo_area = repo_area;
self.changes_area = changes_area;
self.graph_area = graph_area;
self.repo_list.focused = self.focus == FocusPanel::Repos;
self.file_list.focused = self.focus == FocusPanel::Changes;
self.git_graph.focused = self.focus == FocusPanel::Graph;
self.file_list.horizontal_layout = self.horizontal_layout;
self.git_graph.horizontal_layout = self.horizontal_layout;
frame.buffer_mut().reset();
self.repo_list.draw(frame, repo_area)?;
self.file_list.draw(frame, changes_area)?;
self.git_graph.draw(frame, graph_area)?;
if self.horizontal_layout {
use ratatui::style::Style;
let buf = frame.buffer_mut();
for (dragging, x_a, x_b) in [
(
self.dragging_border == Some(0),
repo_area.x + repo_area.width.saturating_sub(1),
changes_area.x,
),
(
self.dragging_border == Some(1),
changes_area.x + changes_area.width.saturating_sub(1),
graph_area.x,
),
] {
let color = if dragging {
self.theme.overlay.border_drag_active
} else {
self.theme.overlay.border_drag_idle
};
let style = Style::default().fg(color);
for x in [x_a, x_b] {
for y in repo_area.y..repo_area.y + repo_area.height {
if let Some(cell) = buf.cell_mut(ratatui::layout::Position::new(x, y)) {
cell.set_symbol("█");
cell.set_style(style);
}
}
}
}
} else if self.dragging_border.is_some() {
use ratatui::style::Style;
let style = Style::default().fg(self.theme.overlay.border_drag_active);
let buf = frame.buffer_mut();
for (dragging, y) in [
(self.dragging_border == Some(0), changes_area.y),
(self.dragging_border == Some(1), graph_area.y),
] {
if !dragging {
continue;
}
for x in repo_area.x..repo_area.x + repo_area.width {
if let Some(cell) = buf.cell_mut(ratatui::layout::Position::new(x, y)) {
cell.set_style(style);
}
}
}
}
self.clear_expired_messages();
self.status_bar.focus = self.focus;
self.status_bar.sort_order = self.sort_order;
self.status_bar.error = self.error_message.clone();
self.status_bar.success = self.success_message.clone();
self.status_bar.draw(frame, status_area)?;
self.context_menu.draw(frame, area)?;
self.path_input.draw(frame, area);
self.confirm_dialog.draw(frame, area);
self.theme_picker.draw(frame, area);
self.picker.draw(frame, area);
if let Some(ref version) = self.update_version {
self.draw_update_notification(frame, main_area, version);
}
if self.show_help {
self.draw_help(frame, main_area);
}
Ok(())
}
pub(super) fn draw_update_notification(
&self,
frame: &mut ratatui::Frame,
area: Rect,
version: &str,
) {
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
let t = &self.theme.overlay;
let text = format!(" \u{2191} v{version} \u{00b7} gitpane update ");
let width = text.len() as u16 + 2;
let height = 3;
if area.width < width || area.height < height {
return;
}
let x = match self.update_position {
UpdatePosition::TopRight => area.x + area.width.saturating_sub(width + 1),
UpdatePosition::TopLeft => area.x + 1,
};
let y = area.y;
let rect = Rect::new(x, y, width, height);
let line = Line::from(vec![
Span::styled(" \u{2191} ", Style::default().fg(t.update_toast_arrow)),
Span::styled(
format!("v{version}"),
Style::default().fg(t.update_toast_version),
),
Span::styled(
" \u{00b7} gitpane update ",
Style::default().fg(t.update_toast_install),
),
]);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(t.update_toast_border));
let paragraph = Paragraph::new(line).block(block);
frame.render_widget(ratatui::widgets::Clear, rect);
frame.render_widget(paragraph, rect);
}
pub(super) fn draw_help(&self, frame: &mut ratatui::Frame, area: Rect) {
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
let t = &self.theme.overlay;
let key = |k: &str| Span::styled(format!(" {k:<10}"), Style::default().fg(t.help_key));
let desc = |d: &str| Span::raw(d.to_string());
let section = |title: &str| {
Line::from(Span::styled(
format!(" {title}"),
Style::default().add_modifier(Modifier::BOLD),
))
};
let mut lines = vec![
section("Global"),
Line::from(vec![key("?"), desc("Toggle this help")]),
Line::from(vec![key("Tab"), desc("Cycle focus forward")]),
Line::from(vec![key("Shift+Tab"), desc("Cycle focus backward")]),
Line::from(vec![key("Esc"), desc("Close / go back")]),
Line::from(vec![key("r"), desc("Refresh all repos")]),
Line::from(vec![key("o"), desc("Open repo/worktree")]),
Line::from(vec![key("v"), desc("Review changes (tmux window)")]),
Line::from(vec![key("G"), desc("Attach live tmux session")]),
Line::from(vec![key("y"), desc("Copy to clipboard")]),
Line::from(vec![key("q"), desc("Quit")]),
];
match self.focus {
FocusPanel::Repos => {
lines.push(Line::from(""));
lines.push(section("Repos"));
lines.push(Line::from(vec![key("j / k"), desc("Move up / down")]));
lines.push(Line::from(vec![key("a"), desc("Add repo")]));
lines.push(Line::from(vec![
key("d"),
desc("Remove repo / worktree (confirm)"),
]));
lines.push(Line::from(vec![key("s"), desc("Cycle sort order")]));
lines.push(Line::from(vec![key("w"), desc("Toggle worktrees")]));
lines.push(Line::from(vec![
key("right-click"),
desc("Menu: new worktree, push/pull, …"),
]));
lines.push(Line::from(vec![key("R"), desc("Rescan repos")]));
lines.push(Line::from(vec![key("g"), desc("Open git graph")]));
}
FocusPanel::Changes => {
lines.push(Line::from(""));
lines.push(section("Changes"));
lines.push(Line::from(vec![key("j / k"), desc("Move up / down")]));
lines.push(Line::from(vec![key("Enter"), desc("Open diff view")]));
lines.push(Line::from(vec![key("Esc / h"), desc("Close diff view")]));
}
FocusPanel::Graph => {
lines.push(Line::from(""));
lines.push(section("Graph"));
lines.push(Line::from(vec![key("j / k"), desc("Move up / down")]));
lines.push(Line::from(vec![key("h / l"), desc("Scroll left / right")]));
lines.push(Line::from(vec![key("Enter"), desc("Open commit files")]));
lines.push(Line::from(""));
lines.push(section("Search"));
lines.push(Line::from(vec![key("/"), desc("Search commits")]));
lines.push(Line::from(vec![key("n / N"), desc("Next / prev match")]));
lines.push(Line::from(""));
lines.push(section("View"));
lines.push(Line::from(vec![key("f"), desc("First-parent mode")]));
lines.push(Line::from(vec![key("c"), desc("Collapse / expand branch")]));
lines.push(Line::from(vec![key("H"), desc("Expand all collapsed")]));
}
}
let height = (lines.len() as u16 + 2).min(area.height);
let width = 42u16.min(area.width);
let x = area.x + (area.width.saturating_sub(width)) / 2;
let y = area.y + (area.height.saturating_sub(height)) / 2;
let help_area = Rect::new(x, y, width, height);
let panel_name = match self.focus {
FocusPanel::Repos => "Repos",
FocusPanel::Changes => "Changes",
FocusPanel::Graph => "Graph",
};
let block = Block::default()
.title(format!(" Keybindings \u{2014} {panel_name} "))
.borders(Borders::ALL)
.border_style(Style::default().fg(t.help_border))
.style(Style::default().bg(t.help_bg));
frame.render_widget(ratatui::widgets::Clear, help_area);
let paragraph = Paragraph::new(lines).block(block);
frame.render_widget(paragraph, help_area);
}
}