use crate::components::manager::ComponentManager;
use crate::app::AppState;
use crate::ui::style::{self, Emphasis};
use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::widgets::{Paragraph, List, ListItem, Clear};
use crate::components::manager::utils;
pub fn render_pr_helper(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
let max_idx = state.pr_list.len().saturating_sub(1);
let selected = state.pr_selected.min(max_idx);
let items: Vec<ListItem> = if state.pr_list.is_empty() {
vec![ListItem::new("No PRs found").style(style::text(&state.theme, Emphasis::Muted))]
} else {
state
.pr_list
.iter()
.enumerate()
.map(|(i, pr)| {
let style_item = if i == selected {
style::selection(&state.theme)
} else {
style::body_style(&state.theme)
};
ListItem::new(format!("#{} {}", pr.number, pr.title)).style(style_item)
})
.collect()
};
let title = format!(
"PR helper [{} / {}] (Enter=checkout, o=open, Esc=close)",
selected.saturating_add(1),
state.pr_list.len().max(1)
);
let popup = utils::center_rect(60, 50, area);
frame.render_widget(Clear, popup);
let list = List::new(items)
.style(style::body_style(&state.theme))
.block(style::pane_block(&state.theme, title.to_string(), true));
frame.render_widget(list, popup);
}
pub fn render_log_action_menu(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
use crate::app::actions::LogAction;
use crate::components::manager::event_handlers;
let dirty = state.status_entries.iter().any(|e| e.staged || e.unstaged || e.conflict);
let in_progress = state.workflow_context.as_ref().map_or(false, |ctx| {
matches!(
ctx.state,
crate::app::workflow::WorkflowState::RebaseInProgress
| crate::app::workflow::WorkflowState::CherryPickInProgress
| crate::app::workflow::WorkflowState::MergeInProgress
| crate::app::workflow::WorkflowState::Conflicts
)
});
let block_reason = if in_progress {
Some("blocked: rebase/cherry-pick/merge/conflicts in progress")
} else if dirty {
Some("blocked: requires clean working tree")
} else {
None
};
let actions: Vec<LogAction> = event_handlers::get_available_log_actions(state);
let action_labels: Vec<(String, bool)> = actions.iter().map(|action| {
let base = match action {
LogAction::ShowDetail => "Show detail",
LogAction::CherryPick => "Cherry-pick",
LogAction::Revert => "Revert",
LogAction::Amend => "Amend (HEAD only)",
LogAction::ResetSoft => "Reset to here (soft)",
LogAction::ResetMixed => "Reset to here (mixed)",
LogAction::ResetHard => "Reset to here (hard)",
LogAction::CreateBranch => "Create branch",
LogAction::CreateTag => "Create tag",
};
let blocked = matches!(action, LogAction::CherryPick | LogAction::Revert | LogAction::ResetSoft | LogAction::ResetMixed | LogAction::ResetHard) && block_reason.is_some();
let label = if blocked { format!("{base} ({})", block_reason.unwrap_or_default()) } else { base.to_string() };
(label, blocked)
}).collect();
let max_idx = actions.len().saturating_sub(1);
let selected = state.log_action_selected.min(max_idx);
let items: Vec<ListItem> = action_labels.iter().enumerate().map(|(i, (label, blocked))| {
let style_item = if i == selected {
style::selection(&state.theme)
} else if *blocked {
style::text(&state.theme, Emphasis::Muted)
} else {
style::body_style(&state.theme)
};
ListItem::new(label.as_str()).style(style_item)
}).collect();
let commit_info = state.log_action_commit_hash.as_ref().map_or("Unknown commit".to_string(), |hash| {
state.commits.iter().find(|c| c.hash == *hash)
.map_or(hash.clone(), |c| format!("{} - {}", c.short_hash, c.message))
});
let clean_hint = block_reason.unwrap_or("Ready: clean working tree");
let title = format!("Actions for: {}", commit_info);
let popup_height = (actions.len() + 7).min(28) as u16;
let popup = utils::center_rect(80, popup_height, area);
frame.render_widget(Clear, popup);
let list = List::new(items)
.style(style::body_style(&state.theme))
.block(style::pane_block(&state.theme, title, true));
frame.render_widget(list, popup);
let footer_area = Rect { x: popup.x, y: popup.y.saturating_add(popup.height.saturating_sub(2)), width: popup.width, height: 2 };
frame.render_widget(Paragraph::new(clean_hint).style(style::body_style(&state.theme)), footer_area);
if let Some(msg) = &state.confirm_message {
let confirm_area = utils::center_rect(50, 20, area);
frame.render_widget(Clear, confirm_area);
let text = format!("{msg}\n\nEnter = confirm Esc = cancel");
frame.render_widget(Paragraph::new(text).style(style::body_style(&state.theme)).block(style::pane_block(&state.theme, "Confirm", true)), confirm_area);
}
}
pub fn render_rebase_todo(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
let max_idx = state.rebase_todo.len().saturating_sub(1);
let selected = state.rebase_todo_selected.min(max_idx);
let items: Vec<ListItem> = if state.rebase_todo.is_empty() {
vec![ListItem::new("No rebase todo").style(style::text(&state.theme, Emphasis::Muted))]
} else {
state.rebase_todo.iter().enumerate().map(|(i, line)| {
let display = if state.rebase_todo_editing && i == selected {
format!("{} [editing]", state.rebase_todo_edit_buffer)
} else { line.clone() };
let style_item = if i == selected { style::selection(&state.theme) } else { style::body_style(&state.theme) };
ListItem::new(display).style(style_item)
}).collect()
};
let title = format!(
"Rebase todo{} [{} / {}] (Esc=close)",
if state.rebase_todo_dirty { " *" } else { "" },
selected.saturating_add(1),
state.rebase_todo.len().max(1)
);
let popup = utils::center_rect(60, 50, area);
frame.render_widget(Clear, popup);
frame.render_widget(List::new(items).style(style::body_style(&state.theme)).block(style::pane_block(&state.theme, title, true)), popup);
}
pub fn render_merge_log(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
let popup = utils::center_rect(60, 30, area);
frame.render_widget(Clear, popup);
let lines: Vec<ListItem> = state.merge_notifier_log.iter().rev().take(15).rev()
.map(|l| ListItem::new(l.clone()).style(style::text(&state.theme, Emphasis::Muted)))
.collect();
frame.render_widget(List::new(lines).style(style::body_style(&state.theme)).block(style::pane_block(&state.theme, "Merge notifier log".to_string(), true)), popup);
}
pub fn render_rebase_builder(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
let entries = &state.rebase_session.entries;
let cursor = state.rebase_session.cursor;
let max_idx = entries.len().saturating_sub(1);
let selected = cursor.min(max_idx);
let items: Vec<ListItem> = if entries.is_empty() {
vec![ListItem::new("No commits to rebase").style(style::text(&state.theme, Emphasis::Muted))]
} else {
entries.iter().enumerate().map(|(i, entry)| {
let action_symbol = match entry.action {
crate::app::rebase::RebaseAction::Pick => "p",
crate::app::rebase::RebaseAction::Reword => "r",
crate::app::rebase::RebaseAction::Edit => "e",
crate::app::rebase::RebaseAction::Squash => "s",
crate::app::rebase::RebaseAction::Fixup => "f",
crate::app::rebase::RebaseAction::Drop => "d",
};
let display = format!("{} {} {}", action_symbol, entry.short_hash, entry.message);
let style_item = if i == selected {
style::selection(&state.theme)
} else {
style::body_style(&state.theme)
};
ListItem::new(display).style(style_item)
}).collect()
};
let base_info = state.rebase_session.base_commit
.as_ref()
.map(|h| format!(" onto {}", &h[..8]))
.unwrap_or_else(|| "".to_string());
let title = format!(
"Rebase builder{} [{} / {}] (Enter=execute, Esc=cancel)",
if state.rebase_session.dirty { " *" } else { "" },
selected.saturating_add(1),
entries.len().max(1)
);
let popup_height = (entries.len().min(15) + 8).max(10) as u16;
let popup = utils::center_rect(90, popup_height, area);
frame.render_widget(Clear, popup);
let list = List::new(items)
.style(style::body_style(&state.theme))
.block(style::pane_block(&state.theme, title.as_str(), true));
frame.render_widget(list, popup);
let base_commit_info = format!("Base commit: {}{}", state.rebase_session.base_commit.as_deref().unwrap_or("HEAD~1"), base_info);
let instructions = vec![
"j/k or ↑/↓ = navigate",
"[/] = move up/down",
"p/r/e/s/f/d = change action",
&base_commit_info,
"Enter = start rebase Esc = cancel",
];
let instructions_text = instructions.join(" | ");
let footer_area = Rect {
x: popup.x,
y: popup.y.saturating_add(popup.height.saturating_sub(3)),
width: popup.width,
height: 3
};
frame.render_widget(
Paragraph::new(instructions_text)
.style(style::text(&state.theme, Emphasis::Muted))
.wrap(ratatui::widgets::Wrap { trim: true }),
footer_area
);
}
pub fn render_rebase_recovery(_manager: &ComponentManager, frame: &mut Frame, area: Rect, state: &AppState) {
let recovery_info = match crate::app::rebase::operations::RebaseRecovery::detect_interrupted_rebase(&state.repo_path) {
Ok(Some(info)) => info,
_ => return, };
let title = "Interrupted Rebase Detected";
let message = format!(
"An interactive rebase was interrupted.\n\n\
Type: {}\n\
Current Commit: {}\n\
Progress: {} / {} steps completed\n\
Conflicts: {}\n\n\
Choose how to proceed:",
match recovery_info.recovery_type {
crate::app::rebase::RebaseRecoveryType::Interactive => "Interactive Rebase",
crate::app::rebase::RebaseRecoveryType::Apply => "Apply-style Rebase",
},
recovery_info.current_commit,
recovery_info.completed_steps,
recovery_info.completed_steps + recovery_info.remaining_steps,
if recovery_info.has_conflicts { "Yes - resolve first" } else { "No" }
);
let popup_height = 15;
let popup = utils::center_rect(80, popup_height, area);
frame.render_widget(Clear, popup);
let block = style::pane_block(&state.theme, title, true);
let inner_area = block.inner(popup);
frame.render_widget(block, popup);
let message_paragraph = Paragraph::new(message)
.style(style::body_style(&state.theme))
.wrap(ratatui::widgets::Wrap { trim: true });
frame.render_widget(message_paragraph, inner_area);
let instructions = vec![
"c = Continue rebase",
"a = Abort rebase",
"Esc = Dismiss (you can recover later)",
];
let instructions_text = instructions.join(" | ");
let footer_area = Rect {
x: popup.x,
y: popup.y.saturating_add(popup.height.saturating_sub(2)),
width: popup.width,
height: 2
};
frame.render_widget(
Paragraph::new(instructions_text)
.style(style::text(&state.theme, Emphasis::Muted))
.wrap(ratatui::widgets::Wrap { trim: true }),
footer_area
);
}
pub fn render_reword_modal(_manager: &ComponentManager, _frame: &mut Frame, _area: Rect, _state: &AppState) {}
pub fn render_edit_modal(_manager: &ComponentManager, _frame: &mut Frame, _area: Rect, _state: &AppState) {}