use ratatui::{
prelude::Rect,
style::{Modifier, Style},
text::{Line, Span},
};
use crate::i18n;
use crate::state::modal::PreflightHeaderChips;
use crate::state::{AppState, PreflightTab};
use crate::theme::theme;
use super::helpers::render_header_chips;
struct TabStatus {
complete: bool,
loading: bool,
}
const fn calculate_summary_status(
summary: Option<&crate::state::modal::PreflightSummaryData>,
summary_loading: bool,
) -> TabStatus {
TabStatus {
complete: summary.is_some(),
loading: summary_loading,
}
}
fn calculate_deps_status(
app: &AppState,
item_names: &std::collections::HashSet<String>,
dependency_info: &[crate::state::modal::DependencyInfo],
) -> TabStatus {
let loading =
app.preflight_deps_resolving || app.deps_resolving || app.preflight_deps_items.is_some();
if loading {
return TabStatus {
complete: false,
loading: true,
};
}
let packages_with_deps: std::collections::HashSet<String> = dependency_info
.iter()
.flat_map(|d| d.required_by.iter())
.cloned()
.collect();
let all_packages_have_deps = packages_with_deps.len() == item_names.len();
let complete = item_names.is_empty()
|| dependency_info.is_empty() || all_packages_have_deps;
TabStatus {
complete,
loading: false,
}
}
fn calculate_files_status(
app: &AppState,
item_names: &std::collections::HashSet<String>,
file_info: &[crate::state::modal::PackageFileInfo],
) -> TabStatus {
let loading =
app.preflight_files_resolving || app.files_resolving || app.preflight_files_items.is_some();
if loading {
return TabStatus {
complete: false,
loading: true,
};
}
let file_info_names: std::collections::HashSet<String> =
file_info.iter().map(|f| f.name.clone()).collect();
let complete = (!item_names.is_empty() && file_info_names.len() == item_names.len())
|| (item_names.is_empty() && !app.install_list_files.is_empty());
TabStatus {
complete,
loading: false,
}
}
const fn calculate_services_status(app: &AppState, services_loaded: bool) -> TabStatus {
let loading = app.preflight_services_resolving || app.services_resolving;
let complete = services_loaded || (!loading && !app.install_list_services.is_empty());
TabStatus { complete, loading }
}
fn calculate_sandbox_status(
app: &AppState,
aur_items: &std::collections::HashSet<String>,
sandbox_info: &[crate::logic::sandbox::SandboxInfo],
sandbox_loaded: bool,
) -> TabStatus {
let loading = app.preflight_sandbox_resolving || app.sandbox_resolving;
if loading {
return TabStatus {
complete: false,
loading: true,
};
}
let sandbox_info_names: std::collections::HashSet<String> = sandbox_info
.iter()
.map(|s| s.package_name.clone())
.collect();
let complete = sandbox_loaded
|| (aur_items.is_empty() || sandbox_info_names.len() == aur_items.len())
|| (aur_items.is_empty() && !app.install_list_sandbox.is_empty());
TabStatus {
complete,
loading: false,
}
}
const fn get_status_icon(
status: &TabStatus,
th: &crate::theme::Theme,
) -> (&'static str, ratatui::style::Color) {
if status.loading {
("⟳ ", th.sapphire)
} else if status.complete {
("✓ ", th.green)
} else {
("", th.overlay1)
}
}
fn build_completion_order(statuses: &[TabStatus]) -> Vec<usize> {
statuses
.iter()
.enumerate()
.filter_map(|(i, status)| {
if status.complete && !status.loading {
Some(i)
} else {
None
}
})
.collect()
}
const fn get_completion_highlight_color(
order_idx: usize,
th: &crate::theme::Theme,
) -> ratatui::style::Color {
match order_idx {
0 => th.green, 1 => th.sapphire, 2 => th.mauve, _ => th.overlay1, }
}
const fn is_tab_active(tab_idx: usize, current_tab: PreflightTab) -> bool {
matches!(
(tab_idx, current_tab),
(0, PreflightTab::Summary)
| (1, PreflightTab::Deps)
| (2, PreflightTab::Files)
| (3, PreflightTab::Services)
| (4, PreflightTab::Sandbox)
)
}
fn calculate_tab_color(
is_active: bool,
tab_idx: usize,
completion_order: &[usize],
th: &crate::theme::Theme,
) -> ratatui::style::Color {
if is_active {
return th.mauve;
}
completion_order
.iter()
.position(|&x| x == tab_idx)
.map_or(th.overlay1, |order_idx| {
get_completion_highlight_color(order_idx, th)
})
}
fn calculate_tab_width(label: &str, status_icon: &str, is_active: bool) -> u16 {
let base_width = label.len() + status_icon.len();
if is_active {
u16::try_from(base_width + 2).unwrap_or(u16::MAX) } else {
u16::try_from(base_width).unwrap_or(u16::MAX)
}
}
fn create_status_icon_span(
status_icon: &'static str,
status_color: ratatui::style::Color,
) -> Option<Span<'static>> {
if status_icon.is_empty() {
return None;
}
Some(Span::styled(
status_icon,
Style::default()
.fg(status_color)
.add_modifier(Modifier::BOLD),
))
}
fn create_tab_label_span(
label: &str,
is_active: bool,
is_completed: bool,
tab_color: ratatui::style::Color,
) -> Span<'static> {
let text = if is_active {
format!("[{label}]")
} else {
label.to_string()
};
let modifier = if is_active || is_completed {
Modifier::BOLD
} else {
Modifier::empty()
};
Span::styled(text, Style::default().fg(tab_color).add_modifier(modifier))
}
#[allow(clippy::too_many_arguments)]
fn render_single_tab(
tab_idx: usize,
label: &str,
status: &TabStatus,
is_active: bool,
completion_order: &[usize],
tab_x: u16,
tab_y: u16,
th: &crate::theme::Theme,
tab_rects: &mut [Option<(u16, u16, u16, u16)>; 5],
) -> (Vec<Span<'static>>, u16) {
let (status_icon, status_color) = get_status_icon(status, th);
let tab_color = calculate_tab_color(is_active, tab_idx, completion_order, th);
let tab_width = calculate_tab_width(label, status_icon, is_active);
tab_rects[tab_idx] = Some((tab_x, tab_y, tab_width, 1));
let new_tab_x = tab_x + tab_width;
let mut spans = Vec::new();
if let Some(icon_span) = create_status_icon_span(status_icon, status_color) {
spans.push(icon_span);
}
let is_completed = completion_order.contains(&tab_idx);
spans.push(create_tab_label_span(
label,
is_active,
is_completed,
tab_color,
));
(spans, new_tab_x)
}
fn extract_package_sets(
items: &[crate::state::PackageItem],
) -> (
std::collections::HashSet<String>,
std::collections::HashSet<String>,
) {
let item_names: std::collections::HashSet<String> =
items.iter().map(|i| i.name.clone()).collect();
let aur_items: std::collections::HashSet<String> = items
.iter()
.filter(|p| matches!(p.source, crate::state::Source::Aur))
.map(|i| i.name.clone())
.collect();
(item_names, aur_items)
}
fn build_tab_labels(app: &AppState) -> [String; 5] {
[
i18n::t(app, "app.modals.preflight.tabs.summary"),
i18n::t(app, "app.modals.preflight.tabs.deps"),
i18n::t(app, "app.modals.preflight.tabs.files"),
i18n::t(app, "app.modals.preflight.tabs.services"),
i18n::t(app, "app.modals.preflight.tabs.sandbox"),
]
}
#[allow(clippy::too_many_arguments)]
fn calculate_all_tab_statuses(
app: &AppState,
item_names: &std::collections::HashSet<String>,
aur_items: &std::collections::HashSet<String>,
summary: Option<&crate::state::modal::PreflightSummaryData>,
dependency_info: &[crate::state::modal::DependencyInfo],
file_info: &[crate::state::modal::PackageFileInfo],
services_loaded: bool,
sandbox_info: &[crate::logic::sandbox::SandboxInfo],
sandbox_loaded: bool,
) -> [TabStatus; 5] {
[
calculate_summary_status(summary, app.preflight_summary_resolving),
calculate_deps_status(app, item_names, dependency_info),
calculate_files_status(app, item_names, file_info),
calculate_services_status(app, services_loaded),
calculate_sandbox_status(app, aur_items, sandbox_info, sandbox_loaded),
]
}
fn build_tab_header_spans(
tab_labels: &[String; 5],
statuses: &[TabStatus; 5],
current_tab: PreflightTab,
completion_order: &[usize],
content_rect: Rect,
th: &crate::theme::Theme,
tab_rects: &mut [Option<(u16, u16, u16, u16)>; 5],
) -> Vec<Span<'static>> {
let tab_y = content_rect.y + 2; let mut tab_x = content_rect.x + 1; let mut tab_spans = Vec::new();
for (i, lbl) in tab_labels.iter().enumerate() {
if i > 0 {
tab_spans.push(Span::raw(" "));
tab_x += 2; }
let is_active = is_tab_active(i, current_tab);
let status = &statuses[i];
let (spans, new_tab_x) = render_single_tab(
i,
lbl,
status,
is_active,
completion_order,
tab_x,
tab_y,
th,
tab_rects,
);
tab_spans.extend(spans);
tab_x = new_tab_x;
}
tab_spans
}
#[allow(clippy::missing_const_for_fn)]
fn store_content_rect(app: &mut AppState, content_rect: Rect) {
app.preflight_content_rect = Some((
content_rect.x + 1, content_rect.y + 4, content_rect.width.saturating_sub(2), content_rect.height.saturating_sub(4), ));
}
pub struct TabHeaderContext<'a> {
pub app: &'a mut AppState,
pub content_rect: Rect,
pub current_tab: PreflightTab,
pub header_chips: &'a PreflightHeaderChips,
pub items: &'a [crate::state::PackageItem],
pub summary: Option<&'a crate::state::modal::PreflightSummaryData>,
pub dependency_info: &'a [crate::state::modal::DependencyInfo],
pub file_info: &'a [crate::state::modal::PackageFileInfo],
pub services_loaded: bool,
pub sandbox_info: &'a [crate::logic::sandbox::SandboxInfo],
pub sandbox_loaded: bool,
}
pub fn render_tab_header(ctx: &mut TabHeaderContext<'_>) -> (Line<'static>, Line<'static>) {
let th = theme();
let (item_names, aur_items) = extract_package_sets(ctx.items);
let tab_labels = build_tab_labels(ctx.app);
let statuses = calculate_all_tab_statuses(
ctx.app,
&item_names,
&aur_items,
ctx.summary,
ctx.dependency_info,
ctx.file_info,
ctx.services_loaded,
ctx.sandbox_info,
ctx.sandbox_loaded,
);
let completion_order = build_completion_order(&statuses);
ctx.app.preflight_tab_rects = [None; 5];
let tab_spans = build_tab_header_spans(
&tab_labels,
&statuses,
ctx.current_tab,
&completion_order,
ctx.content_rect,
&th,
&mut ctx.app.preflight_tab_rects,
);
store_content_rect(ctx.app, ctx.content_rect);
let header_chips_line = render_header_chips(ctx.app, ctx.header_chips);
let tab_header_line = Line::from(tab_spans);
(header_chips_line, tab_header_line)
}