use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Tabs, Wrap};
use super::panels;
use super::types::{Panel, TuiState, WizardMode};
pub fn draw(f: &mut Frame, state: &TuiState) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Length(3), Constraint::Min(10), Constraint::Length(3), ])
.split(f.area());
draw_header(f, state, chunks[0]);
draw_tabs(f, state, chunks[1]);
draw_content(f, state, chunks[2]);
draw_footer(f, state, chunks[3]);
if state.show_help {
draw_help_overlay(f);
}
if state.wizard_mode != WizardMode::None {
draw_branch_wizard(f, state);
}
}
fn draw_header(f: &mut Frame, state: &TuiState, area: Rect) {
let target = if state.target_branch.is_empty() {
"..."
} else {
&state.target_branch
};
let bases = if state.base_branches.is_empty() {
"...".to_string()
} else {
state.base_branches.join(", ")
};
let profile = format!("{:?}", state.config.profile.kind);
let status = if state.running {
Span::styled(" [Running] ", Style::default().fg(Color::Yellow).bold())
} else if state.report.is_some() {
Span::styled(" [Complete] ", Style::default().fg(Color::Green).bold())
} else {
Span::styled(" [Ready] ", Style::default().fg(Color::Cyan))
};
let header = Line::from(vec![
Span::styled(" prview ", Style::default().fg(Color::Cyan).bold()),
Span::raw("| Target: "),
Span::styled(target, Style::default().fg(Color::Yellow)),
Span::raw(" | Base: "),
Span::styled(&bases, Style::default().fg(Color::Green)),
Span::raw(" | Profile: "),
Span::styled(&profile, Style::default().fg(Color::Magenta)),
status,
]);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray));
let paragraph = Paragraph::new(header).block(block);
f.render_widget(paragraph, area);
}
fn draw_tabs(f: &mut Frame, state: &TuiState, area: Rect) {
let titles: Vec<Line> = [
Panel::Config,
Panel::Checks,
Panel::Diffs,
Panel::Artifacts,
Panel::Heuristics,
Panel::State,
]
.iter()
.enumerate()
.map(|(i, p)| {
let num = format!("{}:", i + 1);
let label = p.label();
Line::from(vec![
Span::styled(num, Style::default().fg(Color::DarkGray)),
Span::raw(label),
])
})
.collect();
let tabs = Tabs::new(titles)
.block(Block::default().borders(Borders::ALL).title("Panels"))
.select(state.current_panel.index())
.style(Style::default().fg(Color::White))
.highlight_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.divider(" | ");
f.render_widget(tabs, area);
}
fn draw_content(f: &mut Frame, state: &TuiState, area: Rect) {
match state.current_panel {
Panel::Config => panels::config::draw(f, state, area),
Panel::Checks => panels::checks::draw(f, state, area),
Panel::Diffs => panels::diffs::draw(f, state, area),
Panel::Artifacts => panels::artifacts::draw(f, state, area),
Panel::Heuristics => panels::heuristics::draw(f, state, area),
Panel::State => panels::state::draw(f, state, area),
}
}
fn draw_footer(f: &mut Frame, state: &TuiState, area: Rect) {
let footer = Paragraph::new(Line::from(vec![Span::raw(&state.message)]))
.block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::DarkGray))
.title("Status"),
)
.wrap(Wrap { trim: true });
f.render_widget(footer, area);
}
fn draw_help_overlay(f: &mut Frame) {
let area = centered_rect(60, 70, f.area());
f.render_widget(Clear, area);
let help_text = vec![
Line::from(vec![Span::styled(
"prview Keyboard Shortcuts",
Style::default().fg(Color::Cyan).bold(),
)]),
Line::from(""),
Line::from(vec![Span::styled(
"Navigation",
Style::default().fg(Color::Yellow),
)]),
Line::from(" 1-6 Switch to panel"),
Line::from(" Tab Next panel"),
Line::from(" Shift+Tab Previous panel"),
Line::from(" Up/Down Navigate list"),
Line::from(""),
Line::from(vec![Span::styled(
"Actions",
Style::default().fg(Color::Yellow),
)]),
Line::from(" r Run analysis"),
Line::from(" b Branch selection wizard"),
Line::from(" Enter Select/expand item"),
Line::from(" Space Toggle selection"),
Line::from(" d Show full diff (Diffs panel)"),
Line::from(" j/k Scroll diff up/down"),
Line::from(" PgUp/PgDn Scroll diff"),
Line::from(" o Open file (Artifacts)"),
Line::from(" c Copy path (Artifacts)"),
Line::from(""),
Line::from(vec![Span::styled(
"General",
Style::default().fg(Color::Yellow),
)]),
Line::from(" ? Show/hide help"),
Line::from(" q / Esc Quit"),
Line::from(""),
Line::from(vec![Span::styled(
"Press ? or Esc to close",
Style::default().fg(Color::DarkGray),
)]),
];
let help = Paragraph::new(help_text).block(
Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.title(" Help "),
);
f.render_widget(help, area);
}
fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let popup_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(area);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(popup_layout[1])[1]
}
fn draw_branch_wizard(f: &mut Frame, state: &TuiState) {
let area = centered_rect(60, 80, f.area());
f.render_widget(Clear, area);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ])
.split(area);
let title = match state.wizard_mode {
WizardMode::SelectTarget => " Select Target Branch ",
WizardMode::SelectBase => " Select Base Branch(es) ",
WizardMode::None => " Branch Wizard ",
};
let instructions = match state.wizard_mode {
WizardMode::SelectTarget => {
"Use ↑/↓ to navigate, Enter to select, Tab for remote, Esc to cancel"
}
WizardMode::SelectBase => "Space to toggle, Enter when done, Tab for remote, Esc to cancel",
WizardMode::None => "",
};
let title_block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Cyan))
.title(title);
let title_para = Paragraph::new(Line::from(vec![Span::styled(
instructions,
Style::default().fg(Color::DarkGray),
)]))
.block(title_block);
f.render_widget(title_para, chunks[0]);
let source_label = if state.branch_selector.show_local {
"Local"
} else {
"Remote"
};
let filter_text = if state.branch_selector.filter.is_empty() {
format!("[{}] Type to filter...", source_label)
} else {
format!(
"[{}] Filter: {}",
source_label, state.branch_selector.filter
)
};
let filter_block = Block::default().borders(Borders::ALL).title(" Filter ");
let filter_para = Paragraph::new(Line::from(vec![
Span::styled(&filter_text, Style::default().fg(Color::Yellow)),
Span::raw(" "),
Span::styled("[Tab] toggle source", Style::default().fg(Color::DarkGray)),
]))
.block(filter_block);
f.render_widget(filter_para, chunks[1]);
let filtered_branches = state.get_filtered_branches();
let items: Vec<ListItem> = filtered_branches
.iter()
.enumerate()
.map(|(i, branch)| {
let is_selected = i == state.branch_selector.selected;
let is_current = state
.branch_selector
.current_branch
.as_ref()
.map(|c| c == *branch)
.unwrap_or(false);
let is_base_selected = state.branch_selector.selected_bases.contains(branch);
let prefix = if is_base_selected {
"[x] "
} else if is_current {
" * "
} else {
" "
};
let style = if is_selected {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else if is_current {
Style::default().fg(Color::Green)
} else if is_base_selected {
Style::default().fg(Color::Yellow)
} else {
Style::default()
};
ListItem::new(Line::from(vec![
Span::styled(prefix, style),
Span::styled(*branch, style),
]))
})
.collect();
let list = List::new(items).block(
Block::default()
.borders(Borders::ALL)
.title(format!(" Branches ({}) ", filtered_branches.len())),
);
f.render_widget(list, chunks[2]);
let footer_text = if state.wizard_mode == WizardMode::SelectBase
&& !state.branch_selector.selected_bases.is_empty()
{
format!(
"Selected: {}",
state.branch_selector.selected_bases.join(", ")
)
} else {
String::new()
};
let footer_block = Block::default().borders(Borders::ALL);
let footer_para = Paragraph::new(Line::from(vec![Span::styled(
footer_text,
Style::default().fg(Color::Green),
)]))
.block(footer_block);
f.render_widget(footer_para, chunks[3]);
}