use ratatui::{
prelude::Rect,
style::{Modifier, Style},
text::{Line, Span},
};
use crate::i18n;
use crate::state::modal::{
CascadeMode, DependencyInfo, DependencySource, DependencyStatus, PreflightHeaderChips,
PreflightSummaryData,
};
use crate::state::{AppState, PackageItem, PreflightAction};
use crate::theme::theme;
use crate::ui::helpers::ChangeLogger;
use std::fmt::Write;
use std::sync::{Mutex, OnceLock};
#[derive(Clone, Copy, PartialEq, Eq, Default)]
struct IncompleteFlags(u16);
impl IncompleteFlags {
const PREFLIGHT_DEPS: u16 = 1 << 0;
const DEPS: u16 = 1 << 1;
const COMPUTED_DEPS: u16 = 1 << 2;
const PREFLIGHT_FILES: u16 = 1 << 3;
const FILES: u16 = 1 << 4;
const PREFLIGHT_SANDBOX: u16 = 1 << 5;
const SANDBOX: u16 = 1 << 6;
const DEPS_INCOMPLETE: u16 = 1 << 7;
const SHOW_DEPS_INDICATOR: u16 = 1 << 8;
const SHOW_FILES_INDICATOR: u16 = 1 << 9;
const SHOW_SANDBOX_INDICATOR: u16 = 1 << 10;
const fn set(&mut self, bit: u16, enabled: bool) {
if enabled {
self.0 |= bit;
} else {
self.0 &= !bit;
}
}
const fn is_set(self, bit: u16) -> bool {
self.0 & bit != 0
}
}
#[derive(Clone, PartialEq, Eq)]
struct IncompleteLogState {
flags: IncompleteFlags,
item_count: usize,
packages_with_deps_count: usize,
dependency_info_count: usize,
}
fn log_incomplete_state(state: &IncompleteLogState) {
static LOGGER: OnceLock<Mutex<ChangeLogger<IncompleteLogState>>> = OnceLock::new();
let mut logger = LOGGER
.get_or_init(|| Mutex::new(ChangeLogger::new()))
.lock()
.expect("Incomplete log state mutex poisoned");
if logger.should_log(state) {
let flags = state.flags;
tracing::debug!(
"[UI] render_incomplete_data_indicator: preflight_deps_resolving={}, deps_resolving={}, computed_deps_resolving={}, files_resolving={}/{}, sandbox_resolving={}/{}, deps_incomplete={}, items={}, packages_with_deps={}, dependency_info={}, show_deps_indicator={}, show_files_indicator={}, show_sandbox_indicator={}",
flags.is_set(IncompleteFlags::PREFLIGHT_DEPS),
flags.is_set(IncompleteFlags::DEPS),
flags.is_set(IncompleteFlags::COMPUTED_DEPS),
flags.is_set(IncompleteFlags::PREFLIGHT_FILES),
flags.is_set(IncompleteFlags::FILES),
flags.is_set(IncompleteFlags::PREFLIGHT_SANDBOX),
flags.is_set(IncompleteFlags::SANDBOX),
flags.is_set(IncompleteFlags::DEPS_INCOMPLETE),
state.item_count,
state.packages_with_deps_count,
state.dependency_info_count,
flags.is_set(IncompleteFlags::SHOW_DEPS_INDICATOR),
flags.is_set(IncompleteFlags::SHOW_FILES_INDICATOR),
flags.is_set(IncompleteFlags::SHOW_SANDBOX_INDICATOR)
);
}
}
fn get_source_badge(source: &DependencySource) -> (String, ratatui::style::Color) {
let th = theme();
match source {
DependencySource::Official { repo } => {
let repo_lower = repo.to_lowercase();
let color = if crate::index::is_eos_repo(&repo_lower)
|| crate::index::is_cachyos_repo(&repo_lower)
|| crate::index::is_blackarch_repo(&repo_lower)
{
th.sapphire
} else {
th.green
};
(format!(" [{repo}]"), color)
}
DependencySource::Aur => (" [AUR]".to_string(), th.yellow),
DependencySource::Local => (" [local]".to_string(), th.overlay1),
}
}
fn render_summary_data(
app: &AppState,
summary_data: &PreflightSummaryData,
header_chips: &PreflightHeaderChips,
) -> Vec<Line<'static>> {
let th = theme();
let mut lines = Vec::new();
let risk_color = match header_chips.risk_level {
crate::state::modal::RiskLevel::Low => th.green,
crate::state::modal::RiskLevel::Medium => th.yellow,
crate::state::modal::RiskLevel::High => th.red,
};
if !summary_data.risk_reasons.is_empty() {
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.risk_factors"),
Style::default().fg(risk_color).add_modifier(Modifier::BOLD),
)));
for reason in &summary_data.risk_reasons {
let bullet = format!(" • {reason}");
lines.push(Line::from(Span::styled(
bullet,
Style::default().fg(th.subtext1),
)));
}
}
lines.push(Line::from(""));
lines
}
fn render_incomplete_data_indicator(
app: &AppState,
items: &[PackageItem],
dependency_info: &[DependencyInfo],
) -> Vec<Line<'static>> {
let th = theme();
let mut lines = Vec::new();
let item_names: std::collections::HashSet<String> =
items.iter().map(|i| i.name.clone()).collect();
let packages_with_deps: std::collections::HashSet<String> = dependency_info
.iter()
.flat_map(|d| d.required_by.iter())
.cloned()
.collect();
let packages_with_deps_count = packages_with_deps.len();
let deps_resolving = app.preflight_deps_resolving || app.deps_resolving;
let deps_incomplete = !deps_resolving
&& !item_names.is_empty()
&& packages_with_deps_count < items.len()
&& dependency_info.len() < items.len();
let files_resolving = app.preflight_files_resolving || app.files_resolving;
let sandbox_resolving = app.preflight_sandbox_resolving || app.sandbox_resolving;
let show_deps_indicator = deps_resolving || (deps_incomplete && dependency_info.is_empty());
let show_files_indicator = files_resolving;
let show_sandbox_indicator = sandbox_resolving;
let mut flags = IncompleteFlags::default();
flags.set(
IncompleteFlags::PREFLIGHT_DEPS,
app.preflight_deps_resolving,
);
flags.set(IncompleteFlags::DEPS, app.deps_resolving);
flags.set(IncompleteFlags::COMPUTED_DEPS, deps_resolving);
flags.set(
IncompleteFlags::PREFLIGHT_FILES,
app.preflight_files_resolving,
);
flags.set(IncompleteFlags::FILES, app.files_resolving);
flags.set(
IncompleteFlags::PREFLIGHT_SANDBOX,
app.preflight_sandbox_resolving,
);
flags.set(IncompleteFlags::SANDBOX, app.sandbox_resolving);
flags.set(IncompleteFlags::DEPS_INCOMPLETE, deps_incomplete);
flags.set(IncompleteFlags::SHOW_DEPS_INDICATOR, show_deps_indicator);
flags.set(IncompleteFlags::SHOW_FILES_INDICATOR, show_files_indicator);
flags.set(
IncompleteFlags::SHOW_SANDBOX_INDICATOR,
show_sandbox_indicator,
);
log_incomplete_state(&IncompleteLogState {
flags,
item_count: items.len(),
packages_with_deps_count,
dependency_info_count: dependency_info.len(),
});
let has_incomplete_data =
show_deps_indicator || show_files_indicator || show_sandbox_indicator || deps_incomplete;
if has_incomplete_data {
let mut resolving_parts = Vec::new();
if show_deps_indicator {
resolving_parts.push(i18n::t(app, "app.modals.preflight.summary.resolving_deps"));
}
if show_files_indicator {
resolving_parts.push(i18n::t(app, "app.modals.preflight.summary.resolving_files"));
}
if show_sandbox_indicator {
resolving_parts.push(i18n::t(
app,
"app.modals.preflight.summary.resolving_sandbox",
));
}
if !resolving_parts.is_empty() {
lines.push(Line::from(""));
let resolving_text = resolving_parts.join(", ");
lines.push(Line::from(Span::styled(
format!("⟳ {resolving_text}"),
Style::default()
.fg(th.sapphire)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.data_will_update"),
Style::default().fg(th.subtext1),
)));
}
}
lines
}
fn render_dependency_spans(dep: &DependencyInfo) -> Option<Vec<Span<'static>>> {
let th = theme();
let mut spans = Vec::new();
spans.push(Span::styled(" ", Style::default()));
match &dep.status {
DependencyStatus::Conflict { reason } => {
spans.push(Span::styled("⚠ ", Style::default().fg(th.red)));
spans.push(Span::styled(dep.name.clone(), Style::default().fg(th.text)));
if !dep.version.is_empty() {
spans.push(Span::styled(
format!(" {}", dep.version),
Style::default().fg(th.overlay2),
));
}
let (source_badge, badge_color) = get_source_badge(&dep.source);
spans.push(Span::styled(source_badge, Style::default().fg(badge_color)));
spans.push(Span::styled(
format!(" ({reason})"),
Style::default().fg(th.red),
));
}
DependencyStatus::ToUpgrade { current, required } => {
spans.push(Span::styled("↑ ", Style::default().fg(th.yellow)));
spans.push(Span::styled(dep.name.clone(), Style::default().fg(th.text)));
if !dep.version.is_empty() {
let dep_version = &dep.version;
spans.push(Span::styled(
format!(" {dep_version}"),
Style::default().fg(th.overlay2),
));
}
let (source_badge, badge_color) = get_source_badge(&dep.source);
spans.push(Span::styled(source_badge, Style::default().fg(badge_color)));
spans.push(Span::styled(
format!(" ({current} → {required})"),
Style::default().fg(th.yellow),
));
}
_ => return None,
}
Some(spans)
}
fn filter_installed_packages(items: &[PackageItem]) -> Vec<&PackageItem> {
items
.iter()
.filter(|item| crate::index::is_installed(&item.name))
.collect()
}
fn filter_important_deps(dependency_info: &[DependencyInfo]) -> Vec<&DependencyInfo> {
dependency_info
.iter()
.filter(|d| {
matches!(
d.status,
DependencyStatus::Conflict { .. } | DependencyStatus::ToUpgrade { .. }
)
})
.collect()
}
fn group_dependencies_by_package<'a>(
important_deps: &[&'a DependencyInfo],
) -> std::collections::HashMap<String, Vec<&'a DependencyInfo>> {
use std::collections::HashMap;
let mut grouped: HashMap<String, Vec<&DependencyInfo>> = HashMap::new();
for dep in important_deps {
for req_by in &dep.required_by {
grouped.entry(req_by.clone()).or_default().push(*dep);
}
}
grouped
}
fn build_summary_parts(
app: &AppState,
conflict_count: usize,
upgrade_count: usize,
installed_count: usize,
) -> Vec<String> {
let mut summary_parts = Vec::new();
if conflict_count > 0 {
summary_parts.push(i18n::t_fmt1(
app,
"app.modals.preflight.summary.conflict_singular",
conflict_count,
));
}
if upgrade_count > 0 {
summary_parts.push(i18n::t_fmt1(
app,
"app.modals.preflight.summary.upgrade_singular",
upgrade_count,
));
}
if installed_count > 0 {
summary_parts.push(i18n::t_fmt1(
app,
"app.modals.preflight.summary.installed_singular",
installed_count,
));
}
summary_parts
}
fn build_display_items<'a>(
items: &[PackageItem],
grouped: &std::collections::HashMap<String, Vec<&'a DependencyInfo>>,
installed_packages: &[&PackageItem],
) -> Vec<(bool, String, Option<&'a DependencyInfo>)> {
use std::collections::HashSet;
let mut display_items: Vec<(bool, String, Option<&DependencyInfo>)> = Vec::new();
for pkg_name in items.iter().map(|p| &p.name) {
if let Some(pkg_deps) = grouped.get(pkg_name) {
display_items.push((true, pkg_name.clone(), None));
let mut seen_deps = HashSet::new();
for dep in pkg_deps {
if seen_deps.insert(dep.name.as_str()) {
display_items.push((false, String::new(), Some(*dep)));
}
}
}
}
for installed_pkg in installed_packages {
if !grouped.contains_key(&installed_pkg.name) {
display_items.push((true, installed_pkg.name.clone(), None));
display_items.push((false, installed_pkg.name.clone(), None));
}
}
display_items
}
fn render_install_dependencies(
app: &AppState,
items: &[PackageItem],
dependency_info: &[DependencyInfo],
summary_data: Option<&PreflightSummaryData>,
) -> Vec<Line<'static>> {
let th = theme();
let mut lines = Vec::new();
let installed_packages = filter_installed_packages(items);
let important_deps = filter_important_deps(dependency_info);
if important_deps.is_empty() && installed_packages.is_empty() {
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.no_conflicts_or_upgrades"),
Style::default().fg(th.green),
)));
return lines;
}
let grouped = group_dependencies_by_package(&important_deps);
let conflict_count = important_deps
.iter()
.filter(|d| matches!(d.status, DependencyStatus::Conflict { .. }))
.count();
let upgrade_count = important_deps
.iter()
.filter(|d| matches!(d.status, DependencyStatus::ToUpgrade { .. }))
.count();
let installed_count = installed_packages.len();
let summary_parts = build_summary_parts(app, conflict_count, upgrade_count, installed_count);
let header_text = if conflict_count > 0 || installed_count > 0 {
i18n::t_fmt1(
app,
"app.modals.preflight.summary.issues",
summary_parts.join(", "),
)
} else if upgrade_count > 0 {
i18n::t_fmt1(
app,
"app.modals.preflight.summary.summary_label",
summary_parts.join(", "),
)
} else {
i18n::t(app, "app.modals.preflight.summary.summary_no_conflicts")
};
lines.push(Line::from(Span::styled(
header_text,
Style::default()
.fg(if conflict_count > 0 || installed_count > 0 {
th.red
} else {
th.yellow
})
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
let display_items = build_display_items(items, &grouped, &installed_packages);
for (is_header, pkg_name, dep) in &display_items {
if *is_header {
let style = Style::default()
.fg(th.overlay1)
.add_modifier(Modifier::BOLD);
lines.push(Line::from(Span::styled(format!("▶ {pkg_name}"), style)));
if let Some(summary) = summary_data
&& let Some(pkg_summary) = summary.packages.iter().find(|p| p.name == *pkg_name)
{
for note in &pkg_summary.notes {
if note.contains("Required by") {
lines.push(Line::from(Span::styled(
format!(" ⚠ {note}"),
Style::default().fg(th.yellow),
)));
}
}
}
} else if let Some(dep) = dep {
if let Some(spans) = render_dependency_spans(dep) {
lines.push(Line::from(spans));
}
} else if !pkg_name.is_empty() && installed_packages.iter().any(|p| p.name == *pkg_name) {
let spans = vec![
Span::styled(" ", Style::default()),
Span::styled("✓ ", Style::default().fg(th.green)),
Span::styled(pkg_name.clone(), Style::default().fg(th.text)),
Span::styled(" (installed)", Style::default().fg(th.subtext1)),
];
lines.push(Line::from(spans));
}
}
lines
}
const CASCADE_PREVIEW_MAX: usize = 8;
struct CascadeRenderingContext {
removal_targets: std::collections::HashSet<String>,
allows_dependents: bool,
}
impl CascadeRenderingContext {
fn new(items: &[PackageItem], cascade_mode: CascadeMode) -> Self {
let removal_targets: std::collections::HashSet<String> = items
.iter()
.map(|pkg| pkg.name.to_ascii_lowercase())
.collect();
let allows_dependents = cascade_mode.allows_dependents();
Self {
removal_targets,
allows_dependents,
}
}
fn is_direct_dependent(&self, dep: &DependencyInfo) -> bool {
dep.depends_on
.iter()
.any(|parent| self.removal_targets.contains(&parent.to_ascii_lowercase()))
}
}
struct DependencyDisplayInfo {
bullet: &'static str,
name_color: ratatui::style::Color,
detail: String,
roots: String,
}
const fn get_bullet_and_color(
allows_dependents: bool,
is_direct: bool,
th: &crate::theme::Theme,
) -> (&'static str, ratatui::style::Color) {
if allows_dependents {
if is_direct {
("● ", th.red)
} else {
("○ ", th.yellow)
}
} else if is_direct {
("⛔ ", th.red)
} else {
("⚠ ", th.yellow)
}
}
fn get_dependency_detail(app: &AppState, status: &DependencyStatus) -> String {
match status {
DependencyStatus::Conflict { reason } => reason.clone(),
DependencyStatus::ToUpgrade { .. } => {
i18n::t(app, "app.modals.preflight.summary.requires_version_change")
}
DependencyStatus::Installed { .. } => {
i18n::t(app, "app.modals.preflight.summary.already_satisfied")
}
DependencyStatus::ToInstall => {
i18n::t(app, "app.modals.preflight.summary.not_currently_installed")
}
DependencyStatus::Missing => i18n::t(app, "app.modals.preflight.summary.missing"),
}
}
fn build_dependency_display_info(
app: &AppState,
dep: &DependencyInfo,
ctx: &CascadeRenderingContext,
th: &crate::theme::Theme,
is_direct: bool,
) -> DependencyDisplayInfo {
let (bullet, name_color) = get_bullet_and_color(ctx.allows_dependents, is_direct, th);
let detail = get_dependency_detail(app, &dep.status);
let roots = if dep.required_by.is_empty() {
String::new()
} else {
i18n::t_fmt1(
app,
"app.modals.preflight.summary.targets_label",
dep.required_by.join(", "),
)
};
DependencyDisplayInfo {
bullet,
name_color,
detail,
roots,
}
}
fn build_cascade_dependency_spans(
dep: &DependencyInfo,
display_info: &DependencyDisplayInfo,
th: &crate::theme::Theme,
) -> Vec<Span<'static>> {
let mut spans = Vec::new();
spans.push(Span::styled(
display_info.bullet,
Style::default().fg(th.subtext0),
));
spans.push(Span::styled(
dep.name.clone(),
Style::default()
.fg(display_info.name_color)
.add_modifier(Modifier::BOLD),
));
if !display_info.detail.is_empty() {
spans.push(Span::styled(" — ", Style::default().fg(th.subtext1)));
spans.push(Span::styled(
display_info.detail.clone(),
Style::default().fg(th.subtext1),
));
}
if !display_info.roots.is_empty() {
spans.push(Span::styled(
display_info.roots.clone(),
Style::default().fg(th.overlay1),
));
}
spans
}
fn render_cascade_mode_header(app: &AppState, cascade_mode: CascadeMode) -> Vec<Line<'static>> {
let th = theme();
let mode_line = i18n::t_fmt(
app,
"app.modals.preflight.summary.cascade_mode",
&[&cascade_mode.flag(), &cascade_mode.description()],
);
vec![
Line::from(Span::styled(
mode_line,
Style::default().fg(th.mauve).add_modifier(Modifier::BOLD),
)),
Line::from(""),
]
}
fn render_removal_plan(
app: &AppState,
items: &[PackageItem],
cascade_mode: CascadeMode,
) -> Vec<Line<'static>> {
let th = theme();
let removal_names: Vec<&str> = items.iter().map(|pkg| pkg.name.as_str()).collect();
let plan_header_style = Style::default()
.fg(th.overlay1)
.add_modifier(Modifier::BOLD);
let tool = match crate::logic::privilege::active_tool() {
Ok(t) => t,
Err(msg) => {
return vec![
Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.removal_plan_preview"),
plan_header_style,
)),
Line::from(Span::styled(msg, Style::default().fg(th.red))),
];
}
};
let mut plan_command = crate::logic::privilege::build_privilege_command(
tool,
&format!(
"pacman {} --noconfirm {}",
cascade_mode.flag(),
removal_names.join(" ")
),
);
if app.dry_run {
plan_command = i18n::t_fmt1(
app,
"app.modals.preflight.summary.dry_run_prefix",
plan_command,
);
}
vec![
Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.removal_plan_preview"),
plan_header_style,
)),
Line::from(Span::styled(plan_command, Style::default().fg(th.text))),
]
}
fn get_dependent_summary(
app: &AppState,
dependent_count: usize,
allows_dependents: bool,
) -> (String, Style) {
let th = theme();
if dependent_count == 0 {
(
i18n::t(app, "app.modals.preflight.summary.no_dependents"),
Style::default().fg(th.green),
)
} else if allows_dependents {
(
i18n::t_fmt1(
app,
"app.modals.preflight.summary.cascade_will_include",
dependent_count,
),
Style::default().fg(th.yellow),
)
} else {
(
i18n::t_fmt1(
app,
"app.modals.preflight.summary.dependents_block_removal",
dependent_count,
),
Style::default().fg(th.red),
)
}
}
fn render_dependent_summary(
app: &AppState,
dependent_count: usize,
allows_dependents: bool,
) -> Vec<Line<'static>> {
let (summary_text, summary_style) =
get_dependent_summary(app, dependent_count, allows_dependents);
vec![
Line::from(Span::styled(summary_text, summary_style)),
Line::from(""),
]
}
fn render_impact_overview(app: &AppState) -> Vec<Line<'static>> {
let th = theme();
let mut lines = Vec::new();
if app.remove_preflight_summary.is_empty() {
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.calculating_reverse_deps"),
Style::default().fg(th.subtext1),
)));
} else {
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.removal_impact_overview"),
Style::default()
.fg(th.overlay1)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
for summary in &app.remove_preflight_summary {
let mut message = i18n::t_fmt(
app,
"app.modals.preflight.summary.dependent_singular",
&[&summary.package, &summary.total_dependents],
);
if summary.direct_dependents > 0 {
let _ = write!(
message,
" {}",
i18n::t_fmt1(
app,
"app.modals.preflight.summary.direct_singular",
summary.direct_dependents
)
);
}
if summary.transitive_dependents > 0 {
let _ = write!(
message,
" {}",
i18n::t_fmt1(
app,
"app.modals.preflight.summary.transitive_singular",
summary.transitive_dependents
)
);
}
lines.push(Line::from(Span::styled(
message,
Style::default().fg(th.text),
)));
}
lines.push(Line::from(""));
}
lines
}
fn get_impact_header(app: &AppState, allows_dependents: bool) -> (String, Style) {
let th = theme();
let text = if allows_dependents {
i18n::t(app, "app.modals.preflight.summary.cascade_will_remove")
} else {
i18n::t(app, "app.modals.preflight.summary.dependents_not_removed")
};
(
text,
Style::default().fg(th.red).add_modifier(Modifier::BOLD),
)
}
fn prepare_cascade_candidates<'a>(
dependency_info: &'a [DependencyInfo],
ctx: &CascadeRenderingContext,
) -> Vec<(&'a DependencyInfo, bool)> {
let mut candidates: Vec<(&'a DependencyInfo, bool)> = dependency_info
.iter()
.map(|dep| (dep, ctx.is_direct_dependent(dep)))
.collect();
candidates.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.name.cmp(&b.0.name)));
candidates
}
fn render_cascade_candidates(
app: &AppState,
candidates: &[(&DependencyInfo, bool)],
ctx: &CascadeRenderingContext,
) -> Vec<Line<'static>> {
let th = theme();
let mut lines = Vec::new();
for (dep, is_direct) in candidates.iter().take(CASCADE_PREVIEW_MAX) {
let display_info = build_dependency_display_info(app, dep, ctx, &th, *is_direct);
let spans = build_cascade_dependency_spans(dep, &display_info, &th);
lines.push(Line::from(spans));
}
if candidates.len() > CASCADE_PREVIEW_MAX {
lines.push(Line::from(Span::styled(
i18n::t_fmt1(
app,
"app.modals.preflight.summary.and_more_impacted",
candidates.len() - CASCADE_PREVIEW_MAX,
),
Style::default().fg(th.subtext1),
)));
}
lines
}
fn render_cascade_footer(app: &AppState, allows_dependents: bool) -> Vec<Line<'static>> {
let th = theme();
let mut lines = vec![Line::from("")];
let footer_text = if allows_dependents {
i18n::t(app, "app.modals.preflight.summary.will_be_removed_auto")
} else {
i18n::t(app, "app.modals.preflight.summary.enable_cascade_mode")
};
lines.push(Line::from(Span::styled(
footer_text,
Style::default().fg(th.subtext1),
)));
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.use_deps_tab"),
Style::default().fg(th.subtext1),
)));
lines
}
fn render_remove_action(
app: &AppState,
items: &[PackageItem],
dependency_info: &[DependencyInfo],
cascade_mode: CascadeMode,
) -> Vec<Line<'static>> {
let mut lines = Vec::new();
let th = theme();
lines.extend(render_cascade_mode_header(app, cascade_mode));
if items.is_empty() {
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.no_removal_targets"),
Style::default().fg(th.subtext1),
)));
return lines;
}
lines.extend(render_removal_plan(app, items, cascade_mode));
let ctx = CascadeRenderingContext::new(items, cascade_mode);
let dependent_count = dependency_info.len();
lines.extend(render_dependent_summary(
app,
dependent_count,
ctx.allows_dependents,
));
if dependent_count > 0 {
lines.extend(render_impact_overview(app));
let (impact_header, impact_style) = get_impact_header(app, ctx.allows_dependents);
lines.push(Line::from(Span::styled(impact_header, impact_style)));
let candidates = prepare_cascade_candidates(dependency_info, &ctx);
lines.extend(render_cascade_candidates(app, &candidates, &ctx));
lines.extend(render_cascade_footer(app, ctx.allows_dependents));
}
lines
}
#[allow(clippy::too_many_arguments)]
pub fn render_summary_tab(
app: &AppState,
items: &[PackageItem],
action: PreflightAction,
summary: Option<&PreflightSummaryData>,
header_chips: &PreflightHeaderChips,
dependency_info: &[DependencyInfo],
cascade_mode: CascadeMode,
_content_rect: Rect,
) -> Vec<Line<'static>> {
let th = theme();
let mut lines = Vec::new();
if let Some(summary_data) = summary.as_ref() {
lines.extend(render_summary_data(app, summary_data, header_chips));
} else {
lines.push(Line::from(Span::styled(
"Computing summary...",
Style::default().fg(th.overlay1),
)));
lines.push(Line::from(""));
}
lines.extend(render_incomplete_data_indicator(
app,
items,
dependency_info,
));
match action {
PreflightAction::Install if !dependency_info.is_empty() => {
lines.extend(render_install_dependencies(
app,
items,
dependency_info,
summary,
));
}
PreflightAction::Remove => {
lines.extend(render_remove_action(
app,
items,
dependency_info,
cascade_mode,
));
}
PreflightAction::Install | PreflightAction::Downgrade => {
if items.is_empty() {
lines.push(Line::from(Span::styled(
i18n::t(app, "app.modals.preflight.summary.no_items_selected"),
Style::default().fg(th.subtext1),
)));
} else {
lines.push(Line::from(Span::styled(
i18n::t_fmt1(
app,
"app.modals.preflight.summary.packages_selected",
items.len(),
),
Style::default().fg(th.text),
)));
}
}
}
lines
}