use std::path::PathBuf;
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, List, ListItem, Paragraph},
};
use crate::prelude::*;
use input::{Key, KeyEvent};
use operation::{Event, GitPullOp, Operation};
use settings::Settings;
use views::View;
#[derive(Debug, Clone)]
pub enum PullPhase {
Fetching,
FastForwarding {
upstream: String,
current_branch: String,
},
AwaitingChoice {
current_branch: String,
upstream: String,
},
ApplyingRebase,
ApplyingMerge,
Done(String),
Failed(String),
}
const CHOICES: &[&str] = &["Rebase (recommended)", "Merge", "Cancel"];
pub const DEFAULT_CHOICE: usize = 0;
#[derive(Debug)]
pub struct GitPullView {
pub repo_path: PathBuf,
pub phase: PullPhase,
pub cursor: usize,
}
impl GitPullView {
pub fn new(repo_path: PathBuf) -> Self {
Self {
repo_path,
phase: PullPhase::Fetching,
cursor: DEFAULT_CHOICE,
}
}
fn move_up(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
}
}
fn move_down(&mut self) {
if self.cursor + 1 < CHOICES.len() {
self.cursor += 1;
}
}
pub fn is_terminal(&self) -> bool {
matches!(self.phase, PullPhase::Done(_) | PullPhase::Failed(_))
}
}
impl View for GitPullView {
const KIND: views::ViewKind = views::ViewKind::Primary;
fn save_state(&mut self, _app: &mut crate::app_state::AppState) {}
fn handle_key(&self, key: KeyEvent) -> Vec<Operation> {
match &self.phase {
PullPhase::AwaitingChoice { .. } => match key.key {
Key::ArrowUp => vec![Operation::GitPullLocal(GitPullOp::ChoiceUp)],
Key::ArrowDown => vec![Operation::GitPullLocal(GitPullOp::ChoiceDown)],
Key::Enter => vec![Operation::GitPullLocal(GitPullOp::ChoiceConfirm)],
Key::Escape => vec![Operation::Close],
_ => vec![],
},
_ => match key.key {
Key::Escape | Key::Enter => vec![Operation::Close],
_ => vec![],
},
}
}
fn handle_operation(&mut self, op: &Operation, _settings: &Settings) -> Option<Event> {
match op {
Operation::GitPullLocal(gp_op) => {
match gp_op {
GitPullOp::FetchCompleted {
can_fast_forward,
upstream,
current_branch,
} => {
if *can_fast_forward {
self.phase = PullPhase::FastForwarding {
upstream: upstream.clone(),
current_branch: current_branch.clone(),
};
} else {
self.phase = PullPhase::AwaitingChoice {
current_branch: current_branch.clone(),
upstream: upstream.clone(),
};
self.cursor = DEFAULT_CHOICE;
}
}
GitPullOp::FetchError(msg) => {
self.phase = PullPhase::Failed(format!("Fetch failed: {msg}"));
}
GitPullOp::FastForwardCompleted => {
let upstream = match &self.phase {
PullPhase::FastForwarding { upstream, .. } => upstream.clone(),
_ => "upstream".to_owned(),
};
self.phase = PullPhase::Done(format!("Fast-forwarded to {upstream}"));
}
GitPullOp::FastForwardError(msg) => {
self.phase = PullPhase::Failed(format!("Fast-forward failed: {msg}"));
}
GitPullOp::RebaseCompleted => {
self.phase = PullPhase::Done("Rebase complete".to_owned());
}
GitPullOp::RebaseError(msg) => {
self.phase = PullPhase::Failed(format!("Rebase failed: {msg}"));
}
GitPullOp::MergeCompleted => {
self.phase = PullPhase::Done("Merge complete".to_owned());
}
GitPullOp::MergeError(msg) => {
self.phase = PullPhase::Failed(format!("Merge failed: {msg}"));
}
GitPullOp::ChoiceUp => {
self.move_up();
}
GitPullOp::ChoiceDown => {
self.move_down();
}
GitPullOp::ChoiceConfirm => {}
}
Some(Event::applied("git_pull", op.clone()))
}
Operation::NavigateUp => {
if matches!(self.phase, PullPhase::AwaitingChoice { .. }) {
self.move_up();
Some(Event::applied("git_pull", op.clone()))
} else {
None
}
}
Operation::NavigateDown => {
if matches!(self.phase, PullPhase::AwaitingChoice { .. }) {
self.move_down();
Some(Event::applied("git_pull", op.clone()))
} else {
None
}
}
_ => None,
}
}
fn render(&self, frame: &mut Frame, area: Rect, theme: &crate::theme::Theme) {
render_view(self, frame, area, theme);
}
fn status_bar(
&self,
_state: &crate::app_state::AppState,
bar: &mut crate::widgets::status_bar::StatusBarBuilder,
) {
bar.label("Git: Pull");
}
}
fn render_view(view: &GitPullView, frame: &mut Frame, area: Rect, theme: &crate::theme::Theme) {
let bg = theme.bg();
frame.render_widget(
Block::default().style(Style::default().bg(bg)),
area,
);
let content_height: u16 = match &view.phase {
PullPhase::AwaitingChoice { .. } => 12,
_ => 6,
};
let content_height = content_height.min(area.height);
let vert_pad = area.height.saturating_sub(content_height) / 2;
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(vert_pad),
Constraint::Length(content_height),
Constraint::Min(0),
])
.split(area);
let content_area = chunks[1];
let horiz_pad = (area.width / 6).min(8);
let inner_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(horiz_pad),
Constraint::Min(1),
Constraint::Length(horiz_pad),
])
.split(content_area);
let inner = inner_chunks[1];
match &view.phase {
PullPhase::Fetching => render_status(
"Fetching from remote\u{2026}",
None,
"[Esc: cancel]",
inner,
frame,
theme,
),
PullPhase::FastForwarding { upstream, .. } => render_status(
&format!("Fast-forwarding to {upstream}\u{2026}"),
None,
"",
inner,
frame,
theme,
),
PullPhase::ApplyingRebase => render_status(
"Rebasing onto upstream\u{2026}",
None,
"",
inner,
frame,
theme,
),
PullPhase::ApplyingMerge => render_status(
"Merging upstream\u{2026}",
None,
"",
inner,
frame,
theme,
),
PullPhase::Done(msg) => render_status(
&format!("\u{2713} {msg}"),
None,
"[Esc: close]",
inner,
frame,
theme,
),
PullPhase::Failed(msg) => render_status(
&format!("\u{2717} {msg}"),
None,
"[Esc: close]",
inner,
frame,
theme,
),
PullPhase::AwaitingChoice {
current_branch,
upstream,
} => render_choice(view, current_branch, upstream, inner, frame, theme),
}
}
fn render_status(
message: &str,
_detail: Option<&str>,
hint: &str,
area: Rect,
frame: &mut Frame,
theme: &crate::theme::Theme,
) {
let fg = theme.fg();
let fg_dim = theme.fg_dim();
let bg = theme.bg();
let base = Style::default().bg(bg);
frame.render_widget(
Paragraph::new(Span::styled(
message,
Style::default().fg(fg).bg(bg).add_modifier(Modifier::BOLD),
))
.alignment(Alignment::Center),
area,
);
if !hint.is_empty() && area.height >= 2 {
let hint_area = Rect {
y: area.y + area.height - 1,
height: 1,
..area
};
frame.render_widget(
Paragraph::new(Span::styled(hint, Style::default().fg(fg_dim).bg(bg)))
.style(base)
.alignment(Alignment::Center),
hint_area,
);
}
}
fn render_choice(
view: &GitPullView,
current_branch: &str,
upstream: &str,
area: Rect,
frame: &mut Frame,
theme: &crate::theme::Theme,
) {
let fg = theme.fg();
let fg_dim = theme.fg_dim();
let accent = theme.accent();
let sel_bg = theme.selection_bg();
let sel_fg = theme.selection_fg();
let bg = theme.bg();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), Constraint::Length(CHOICES.len() as u16), Constraint::Min(0), Constraint::Length(1), ])
.split(area);
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled("Branch ", Style::default().fg(fg_dim).bg(bg)),
Span::styled(
current_branch,
Style::default()
.fg(accent)
.bg(bg)
.add_modifier(Modifier::BOLD),
),
Span::styled(
" has diverged from ",
Style::default().fg(fg_dim).bg(bg),
),
Span::styled(
upstream,
Style::default()
.fg(accent)
.bg(bg)
.add_modifier(Modifier::BOLD),
),
])),
rows[0],
);
frame.render_widget(
Paragraph::new(Span::styled(
"Choose how to integrate the upstream changes:",
Style::default().fg(fg).bg(bg),
)),
rows[1],
);
let items: Vec<ListItem> = CHOICES
.iter()
.enumerate()
.map(|(i, label)| {
let selected = i == view.cursor;
let item_bg = if selected { sel_bg } else { bg };
let item_fg = if selected { sel_fg } else { fg };
let indicator = if selected { "\u{25b6} " } else { " " };
ListItem::new(Line::from(vec![
Span::styled(
indicator,
Style::default()
.fg(if selected { accent } else { fg_dim })
.bg(item_bg),
),
Span::styled(
*label,
Style::default()
.fg(item_fg)
.bg(item_bg)
.add_modifier(if selected {
Modifier::BOLD
} else {
Modifier::empty()
}),
),
]))
})
.collect();
frame.render_widget(
List::new(items)
.block(Block::default().borders(Borders::NONE))
.style(Style::default().bg(bg)),
rows[3],
);
frame.render_widget(
Paragraph::new(Span::styled(
"[\u{2191}\u{2193}: navigate] [Enter: confirm] [Esc: cancel]",
Style::default().fg(fg_dim).bg(bg),
))
.alignment(Alignment::Center),
rows[5],
);
}