use std::collections::HashSet;
use color_eyre::Result;
use ratatui::widgets::{ListState, TableState};
use synh8::core::{ManagerState, check_apt_lock};
use synh8::progress::{ProgressState, StdioRedirect, TuiAcquireProgress, TuiInstallProgress};
use synh8::types::*;
pub struct UiState {
pub table_state: TableState,
pub filter_state: ListState,
pub focused_pane: FocusedPane,
pub visual_range: Option<(usize, usize)>,
pub selection_anchor: Option<usize>,
pub visual_mode: bool,
pub table_visible_rows: usize,
}
pub struct DetailsState {
pub scroll: u16,
pub tab: DetailsTab,
pub cached_deps: Vec<(String, String)>,
pub cached_rdeps: Vec<(String, String)>,
pub cached_pkg_name: String,
}
impl Default for DetailsState {
fn default() -> Self {
Self {
scroll: 0,
tab: DetailsTab::Info,
cached_deps: Vec::new(),
cached_rdeps: Vec::new(),
cached_pkg_name: String::new(),
}
}
}
#[derive(Default)]
pub struct ModalState {
pub changes_scroll: u16,
pub changelog_scroll: u16,
pub changelog_content: Vec<String>,
}
pub struct App {
pub core: ManagerState,
pub ui: UiState,
pub details: DetailsState,
pub modals: ModalState,
pub state: AppState,
pub settings: Settings,
pub settings_selection: usize,
pub col_widths: ColumnWidths,
pub status_message: String,
pub output_lines: Vec<String>,
pub mark_preview: Option<MarkPreview>,
pub mark_preview_scroll: usize,
pub output_scroll: u16,
pub warm_step: Option<usize>,
}
impl App {
pub fn new() -> Result<Self> {
let core = ManagerState::new()?;
let mut filter_state = ListState::default();
filter_state.select(Some(0));
let settings = Settings::default();
let mut app = Self {
core,
ui: UiState {
table_state: TableState::default(),
filter_state,
focused_pane: FocusedPane::Packages,
visual_range: None,
selection_anchor: None,
visual_mode: false,
table_visible_rows: 0,
},
details: DetailsState::default(),
modals: ModalState::default(),
state: AppState::Listing,
settings,
settings_selection: 0,
col_widths: ColumnWidths::new(),
status_message: String::from("Loading..."),
output_lines: Vec::new(),
mark_preview: None,
mark_preview_scroll: 0,
output_scroll: 0,
warm_step: Some(0),
};
app.core.set_sort(app.settings.sort_by, app.settings.sort_ascending);
app.refresh_ui_state();
app.update_status_message();
Ok(app)
}
pub fn warm_next(&mut self) -> bool {
let step = match self.warm_step {
Some(s) => s,
None => return false,
};
let filters = FilterCategory::all();
let current_filter = self.core.selected_filter();
if step < filters.len() {
let filter = filters[step];
if filter != current_filter {
self.core.set_filter(filter);
self.core.rebuild_list();
}
if filter != current_filter {
self.core.set_filter(current_filter);
self.core.rebuild_list();
}
self.warm_step = Some(step + 1);
true
} else if step == filters.len() {
let _ = self.core.ensure_search_index();
self.warm_step = None;
false
} else {
self.warm_step = None;
false
}
}
#[hotpath::measure]
pub fn refresh_ui_state(&mut self) {
let selected_name = self.selected_package().map(|p| p.name.clone());
self.col_widths = self.core.rebuild_list();
self.restore_selection(selected_name);
self.update_cached_deps();
}
#[hotpath::measure]
fn restore_selection(&mut self, package_name: Option<String>) {
self.ui.visual_range = None;
self.ui.selection_anchor = None;
self.ui.visual_mode = false;
let new_idx = package_name
.and_then(|name| self.core.list().iter().position(|p| p.name == name))
.unwrap_or(0);
self.ui.table_state.select(if self.core.package_count() > 0 {
Some(new_idx)
} else {
None
});
self.center_scroll_offset();
}
fn reset_selection(&mut self) {
self.restore_selection(None);
}
pub fn selected_package(&self) -> Option<&PackageInfo> {
self.ui.table_state
.selected()
.and_then(|i| self.core.get_package(i))
}
#[must_use]
pub fn has_pending_changes(&self) -> bool {
self.core.has_marks()
}
#[must_use]
pub fn total_changes_count(&self) -> usize {
match self.core.planned_changes() {
Some(changes) => changes.len(),
None => 0,
}
}
#[hotpath::measure]
pub fn update_cached_deps(&mut self) {
let pkg_name = self.selected_package()
.map(|p| p.name.clone())
.unwrap_or_default();
if pkg_name == self.details.cached_pkg_name {
return;
}
self.details.cached_pkg_name = pkg_name.clone();
self.details.cached_deps = self.core.get_dependencies(&pkg_name);
self.details.cached_rdeps = self.core.get_reverse_dependencies(&pkg_name);
}
pub fn start_search(&mut self) {
match self.core.ensure_search_index() {
Ok(duration) => {
if duration.as_millis() > 0 {
self.status_message = format!(
"Search index built in {:.0}ms",
duration.as_secs_f64() * 1000.0
);
}
}
Err(e) => {
self.status_message = format!("Failed to build search index: {e}");
return;
}
}
self.state = AppState::Searching;
}
pub fn execute_search(&mut self) {
let query = self.core.search_query().to_string();
if let Err(e) = self.core.set_search_query(&query) {
self.status_message = format!("Search error: {e}");
}
self.refresh_ui_state();
}
pub fn cancel_search(&mut self) {
self.core.clear_search();
self.state = AppState::Listing;
self.refresh_ui_state();
self.update_status_message();
}
pub fn confirm_search(&mut self) {
self.state = AppState::Listing;
if let Some(count) = self.core.search_result_count() {
self.status_message = format!(
"Found {} packages matching '{}'",
count,
self.core.search_query()
);
}
}
pub fn apply_current_filter(&mut self) {
self.col_widths = self.core.rebuild_list();
self.reset_selection();
}
pub fn select_first_filter(&mut self) {
self.move_filter_selection(-(FilterCategory::all().len() as i32));
}
pub fn select_last_filter(&mut self) {
self.move_filter_selection(FilterCategory::all().len() as i32);
}
pub fn move_filter_selection(&mut self, delta: i32) {
if self.ui.visual_mode {
self.cancel_visual_mode();
}
let filters = FilterCategory::all();
let current = self.ui.filter_state.selected().unwrap_or(0) as i32;
let new_idx = (current + delta).clamp(0, filters.len() as i32 - 1) as usize;
self.ui.filter_state.select(Some(new_idx));
self.core.set_filter(filters[new_idx]);
self.refresh_ui_state();
}
pub fn toggle_current(&mut self) {
let Some(pkg) = self.selected_package() else {
return;
};
let id = pkg.id;
let pkg_name = self.core.cache().display_name(&pkg.name).to_string();
let was_marked = pkg.status.is_marked();
if !was_marked && pkg.status == PackageStatus::Installed {
self.status_message = format!("{pkg_name} is already installed and up to date");
return;
}
let was_user_marked = self.core.is_user_marked(id);
let previously_planned: HashSet<PackageId> = self.core.planned_changes()
.map(|changes| changes.iter().map(|c| c.package).collect())
.unwrap_or_default();
let result = self.core.toggle(id);
match result {
ToggleResult::Marked { package: _, additional } => {
if additional.is_empty() {
self.refresh_ui_state();
self.update_status_message();
} else {
let preview = self.core.build_mark_preview(id, &previously_planned);
let needs_confirm = match &preview {
Some(MarkPreview::Mark { additional_installs, additional_removes, .. }) =>
!additional_installs.is_empty() || !additional_removes.is_empty(),
_ => true,
};
if needs_confirm {
self.mark_preview = preview;
self.mark_preview_scroll = 0;
self.state = AppState::ShowingMarkConfirm;
} else {
self.refresh_ui_state();
self.update_status_message();
}
}
}
ToggleResult::Unmarked { package: _, also_unmarked } => {
if also_unmarked.is_empty() {
self.refresh_ui_state();
self.update_status_message();
} else {
let cache = self.core.cache();
let also_names: Vec<String> = also_unmarked.iter()
.filter_map(|id| cache.fullname_of(*id).map(|n| cache.display_name(n).to_string()))
.collect();
let preview = MarkPreview::Unmark {
package_name: pkg_name,
was_user_marked,
also_unmarked: also_names,
bulk_acted_ids: Vec::new(),
};
self.mark_preview = Some(preview);
self.mark_preview_scroll = 0;
self.state = AppState::ShowingMarkConfirm;
}
}
ToggleResult::NoChange { package: _ } => {
self.status_message = format!(
"{pkg_name} is a dependency - unmark the package that requires it"
);
self.refresh_ui_state();
}
}
}
pub fn confirm_mark(&mut self) {
self.mark_preview = None;
self.refresh_ui_state();
self.update_status_message();
self.state = AppState::Listing;
}
fn resolve_display_name_to_id(&self, display_name: &str) -> Option<PackageId> {
let cache = self.core.cache();
if let Some(id) = cache.get_id(display_name) {
return Some(id);
}
let fullname = format!("{}:{}", display_name, cache.native_arch());
cache.get_id(&fullname)
}
pub fn cancel_mark(&mut self) {
if let Some(ref preview) = self.mark_preview {
match preview {
MarkPreview::Mark { package_name, bulk_acted_ids, .. } => {
if !bulk_acted_ids.is_empty() {
for &id in bulk_acted_ids {
self.core.unmark(id);
}
self.core.compute_plan();
} else {
let id_to_unmark = self.resolve_display_name_to_id(package_name);
if let Some(id) = id_to_unmark {
self.core.unmark(id);
}
}
}
MarkPreview::Unmark { package_name, was_user_marked, also_unmarked, bulk_acted_ids } => {
if !bulk_acted_ids.is_empty() {
for &id in bulk_acted_ids {
self.core.mark_install(id);
}
self.core.compute_plan();
} else {
let names_to_remark: Vec<String> = if *was_user_marked {
vec![package_name.clone()]
} else {
also_unmarked.clone()
};
let ids_to_remark: Vec<_> = names_to_remark.iter()
.filter_map(|name| self.resolve_display_name_to_id(name))
.collect();
for id in ids_to_remark {
self.core.mark_install(id);
}
self.core.compute_plan();
}
}
}
}
self.mark_preview = None;
self.refresh_ui_state();
self.update_status_message();
self.state = AppState::Listing;
}
pub fn mark_all_upgrades(&mut self) {
self.core.mark_all_upgradable();
self.update_status_message();
self.show_changes_preview();
}
pub fn unmark_all(&mut self) {
self.core.reset();
self.refresh_ui_state();
self.update_status_message();
}
pub fn start_visual_mode(&mut self) {
let current_idx = self.ui.table_state.selected().unwrap_or(0);
if !self.ui.visual_mode {
self.ui.visual_mode = true;
self.ui.selection_anchor = Some(current_idx);
self.ui.visual_range = Some((current_idx, current_idx));
self.status_message = "-- VISUAL -- (↑↓ to select, Space to mark, Esc to cancel)".to_string();
} else {
self.mark_selected_packages();
}
}
pub fn update_visual_selection(&mut self) {
if !self.ui.visual_mode {
return;
}
let current_idx = self.ui.table_state.selected().unwrap_or(0);
if let Some(anchor) = self.ui.selection_anchor {
let start = anchor.min(current_idx);
let end = anchor.max(current_idx);
self.ui.visual_range = Some((start, end));
}
}
pub fn cancel_visual_mode(&mut self) {
self.ui.visual_mode = false;
self.ui.visual_range = None;
self.ui.selection_anchor = None;
self.update_status_message();
}
pub fn toggle_multi_select(&mut self) {
if !self.ui.visual_mode {
self.start_visual_mode();
} else {
self.mark_selected_packages();
}
}
fn mark_selected_packages(&mut self) {
let anchor_idx = match self.ui.selection_anchor {
Some(idx) => idx,
None => {
self.cancel_visual_mode();
return;
}
};
let anchor_is_marked = self.core.get_package(anchor_idx)
.map(|p| p.status.is_marked())
.unwrap_or(false);
let selected_indices: Vec<usize> = match self.ui.visual_range {
Some((start, end)) => (start..=end).collect(),
None => Vec::new(),
};
self.ui.visual_range = None;
self.ui.selection_anchor = None;
self.ui.visual_mode = false;
if anchor_is_marked {
self.bulk_unmark(&selected_indices);
} else {
self.bulk_mark(&selected_indices);
}
}
fn bulk_mark(&mut self, selected_indices: &[usize]) {
let previously_planned: HashSet<PackageId> = self.core.planned_changes()
.map(|changes| changes.iter().map(|c| c.package).collect())
.unwrap_or_default();
let ids_to_mark: Vec<PackageId> = selected_indices.iter()
.filter_map(|&idx| self.core.get_package(idx))
.filter(|p| !p.status.is_marked() && (
p.status == PackageStatus::Upgradable
|| p.status == PackageStatus::NotInstalled
))
.map(|p| p.id)
.collect();
if ids_to_mark.is_empty() {
self.status_message = "No packages to mark in selection".to_string();
return;
}
for &id in &ids_to_mark {
self.core.mark_install(id);
}
self.core.compute_plan();
self.col_widths = self.core.rebuild_list();
let marked_id_set: HashSet<PackageId> = ids_to_mark.iter().copied().collect();
let mut additional_installs = Vec::new();
let mut additional_upgrades = Vec::new();
let mut additional_removes = Vec::new();
let mut download_size = 0u64;
if let Some(changes) = self.core.planned_changes() {
let cache = self.core.cache();
for change in changes {
download_size += change.download_size;
if marked_id_set.contains(&change.package) {
continue;
}
if previously_planned.contains(&change.package) {
continue;
}
let name = cache.fullname_of(change.package)
.map(|n| cache.display_name(n).to_string())
.unwrap_or_else(|| format!("(unknown:{})", change.package.index()));
match change.action {
ChangeAction::Install => additional_installs.push(name),
ChangeAction::Upgrade | ChangeAction::Downgrade => additional_upgrades.push(name),
ChangeAction::Remove => additional_removes.push(name),
}
}
}
let needs_confirm = !additional_installs.is_empty()
|| !additional_removes.is_empty();
if !needs_confirm {
self.refresh_ui_state();
self.update_status_message();
return;
}
let summary_name = if ids_to_mark.len() == 1 {
self.core.cache().fullname_of(ids_to_mark[0])
.map(|n| self.core.cache().display_name(n).to_string())
.unwrap_or_else(|| "1 package".to_string())
} else {
format!("{} packages", ids_to_mark.len())
};
self.mark_preview = Some(MarkPreview::Mark {
package_name: summary_name,
is_upgrade: false,
additional_installs,
additional_upgrades,
additional_removes,
download_size,
bulk_acted_ids: ids_to_mark,
});
self.mark_preview_scroll = 0;
self.state = AppState::ShowingMarkConfirm;
}
fn bulk_unmark(&mut self, selected_indices: &[usize]) {
let marked_before: HashSet<PackageId> = self.core.list().iter()
.filter(|p| p.status.is_marked())
.map(|p| p.id)
.collect();
let ids_to_unmark: Vec<PackageId> = selected_indices.iter()
.filter_map(|&idx| self.core.get_package(idx))
.filter(|p| p.status.is_marked() && self.core.is_user_marked(p.id))
.map(|p| p.id)
.collect();
if ids_to_unmark.is_empty() {
self.status_message = "No user-marked packages to unmark in selection".to_string();
return;
}
for &id in &ids_to_unmark {
self.core.unmark(id);
}
self.core.compute_plan();
self.col_widths = self.core.rebuild_list();
let unmarked_id_set: HashSet<PackageId> = ids_to_unmark.iter().copied().collect();
let cascade_unmarked: Vec<String> = {
let marked_after: HashSet<PackageId> = self.core.list().iter()
.filter(|p| p.status.is_marked())
.map(|p| p.id)
.collect();
let cache = self.core.cache();
marked_before.iter()
.filter(|id| !marked_after.contains(id) && !unmarked_id_set.contains(id))
.filter_map(|id| cache.fullname_of(*id).map(|n| cache.display_name(n).to_string()))
.collect()
};
if cascade_unmarked.is_empty() {
self.refresh_ui_state();
self.update_status_message();
return;
}
let summary_name = if ids_to_unmark.len() == 1 {
self.core.cache().fullname_of(ids_to_unmark[0])
.map(|n| self.core.cache().display_name(n).to_string())
.unwrap_or_else(|| "1 package".to_string())
} else {
format!("{} packages", ids_to_unmark.len())
};
self.mark_preview = Some(MarkPreview::Unmark {
package_name: summary_name,
was_user_marked: true,
also_unmarked: cascade_unmarked,
bulk_acted_ids: ids_to_unmark,
});
self.mark_preview_scroll = 0;
self.state = AppState::ShowingMarkConfirm;
}
pub fn move_package_selection(&mut self, delta: i32) {
if self.core.package_count() == 0 {
return;
}
let current = self.ui.table_state.selected().unwrap_or(0) as i64;
let new_idx = (current + delta as i64).clamp(0, self.core.package_count() as i64 - 1) as usize;
self.ui.table_state.select(Some(new_idx));
self.center_scroll_offset();
self.details.scroll = 0;
self.update_cached_deps();
}
pub fn select_first_package(&mut self) {
self.move_package_selection(-(self.core.package_count() as i32));
}
pub fn select_last_package(&mut self) {
self.move_package_selection(self.core.package_count() as i32);
}
fn center_scroll_offset(&mut self) {
let visible = self.ui.table_visible_rows;
if visible == 0 {
return;
}
let selected = self.ui.table_state.selected().unwrap_or(0);
let total = self.core.package_count();
let half = visible / 2;
let max_offset = total.saturating_sub(visible);
let offset = selected.saturating_sub(half).min(max_offset);
*self.ui.table_state.offset_mut() = offset;
}
pub fn next_details_tab(&mut self) {
self.details.tab = match self.details.tab {
DetailsTab::Info => DetailsTab::Dependencies,
DetailsTab::Dependencies => DetailsTab::ReverseDeps,
DetailsTab::ReverseDeps => DetailsTab::Info,
};
self.details.scroll = 0;
}
pub fn prev_details_tab(&mut self) {
self.details.tab = match self.details.tab {
DetailsTab::Info => DetailsTab::ReverseDeps,
DetailsTab::Dependencies => DetailsTab::Info,
DetailsTab::ReverseDeps => DetailsTab::Dependencies,
};
self.details.scroll = 0;
}
pub fn cycle_focus(&mut self) {
self.ui.focused_pane = match self.ui.focused_pane {
FocusedPane::Filters => FocusedPane::Packages,
FocusedPane::Packages => FocusedPane::Details,
FocusedPane::Details => FocusedPane::Filters,
};
}
pub fn cycle_focus_back(&mut self) {
self.ui.focused_pane = match self.ui.focused_pane {
FocusedPane::Filters => FocusedPane::Details,
FocusedPane::Packages => FocusedPane::Filters,
FocusedPane::Details => FocusedPane::Packages,
};
}
pub fn show_changelog(&mut self) {
let pkg_name = match self.selected_package() {
Some(p) => p.name.clone(),
None => {
self.status_message = "No package selected".to_string();
return;
}
};
self.modals.changelog_content.clear();
self.modals.changelog_content.push(format!("Loading changelog for {pkg_name}..."));
self.modals.changelog_scroll = 0;
match self.core.fetch_changelog(&pkg_name) {
Ok(lines) => {
self.modals.changelog_content = lines;
}
Err(e) => {
self.modals.changelog_content.clear();
self.modals.changelog_content.push(e);
}
}
self.state = AppState::ShowingChangelog;
}
pub fn show_settings(&mut self) {
self.settings_selection = 0;
self.state = AppState::ShowingSettings;
}
pub fn toggle_setting(&mut self) {
let all_cols = Column::all();
let col_count = all_cols.len();
if self.settings_selection < col_count {
let col = all_cols[self.settings_selection];
if !self.settings.visible_columns.remove(&col) {
self.settings.visible_columns.insert(col);
}
} else if self.settings_selection == col_count {
let all = SortBy::all();
let idx = all.iter().position(|&s| s == self.settings.sort_by).unwrap_or(0);
self.settings.sort_by = all[(idx + 1) % all.len()];
self.core.set_sort(self.settings.sort_by, self.settings.sort_ascending);
self.col_widths = self.core.rebuild_list();
} else if self.settings_selection == col_count + 1 {
self.settings.sort_ascending = !self.settings.sort_ascending;
self.core.set_sort(self.settings.sort_by, self.settings.sort_ascending);
self.col_widths = self.core.rebuild_list();
}
}
pub fn settings_item_count() -> usize {
Column::all().len() + 2
}
pub fn show_changes_preview(&mut self) {
if self.has_pending_changes() {
self.core.compute_plan();
self.state = AppState::ShowingChanges;
self.modals.changes_scroll = 0;
} else {
self.status_message = "No changes to apply".to_string();
}
}
pub fn scroll_changelog(&mut self, delta: i32) {
let max = self.modals.changelog_content.len().saturating_sub(1);
self.modals.changelog_scroll = clamped_scroll(self.modals.changelog_scroll.into(), delta, max) as u16;
}
pub fn scroll_changes(&mut self, delta: i32) {
let max = self.changes_line_count().saturating_sub(5);
self.modals.changes_scroll = clamped_scroll(self.modals.changes_scroll.into(), delta, max) as u16;
}
pub fn scroll_mark_confirm(&mut self, delta: i32) {
let max = self.mark_confirm_line_count().saturating_sub(10);
self.mark_preview_scroll = clamped_scroll(self.mark_preview_scroll, delta, max);
}
pub fn scroll_output(&mut self, delta: i32) {
let max = self.output_lines.len().saturating_sub(1);
self.output_scroll = clamped_scroll(self.output_scroll.into(), delta, max) as u16;
}
pub fn changes_line_count(&self) -> usize {
match self.core.planned_changes() {
Some(changes) => {
let mut lines = 2;
let categories = [
changes.iter().filter(|c| c.action == ChangeAction::Upgrade && c.reason == ChangeReason::UserRequested).count(),
changes.iter().filter(|c| c.action == ChangeAction::Install && c.reason == ChangeReason::UserRequested).count(),
changes.iter().filter(|c| c.action == ChangeAction::Upgrade && c.reason == ChangeReason::Dependency).count(),
changes.iter().filter(|c| c.action == ChangeAction::Install && c.reason == ChangeReason::Dependency).count(),
changes.iter().filter(|c| c.action == ChangeAction::Remove && c.reason == ChangeReason::UserRequested).count(),
changes.iter().filter(|c| c.action == ChangeAction::Remove && c.reason == ChangeReason::AutoRemove).count(),
];
for count in categories {
if count > 0 {
lines += 1 + count + 1; }
}
lines += 3; lines
}
None => 5,
}
}
pub fn mark_confirm_line_count(&self) -> usize {
match self.mark_preview {
Some(MarkPreview::Mark { ref additional_installs, ref additional_upgrades, ref additional_removes, .. }) => {
let mut count = 2; if !additional_installs.is_empty() {
count += 1 + additional_installs.len();
}
if !additional_upgrades.is_empty() {
count += 1 + additional_upgrades.len();
}
if !additional_removes.is_empty() {
count += 1 + additional_removes.len();
}
count + 2 }
Some(MarkPreview::Unmark { ref also_unmarked, .. }) => {
let mut count = 2; if !also_unmarked.is_empty() {
count += 1 + also_unmarked.len();
}
count + 2 }
None => 0,
}
}
pub fn update_status_message(&mut self) {
let mark_count = self.core.user_mark_count();
if mark_count > 0 {
self.status_message = format!(
"{mark_count} packages marked | {} upgradable | Press 'a' to apply",
self.core.upgradable_count()
);
} else {
self.status_message = format!("{} packages upgradable", self.core.upgradable_count());
}
}
pub fn commit_changes_live(&mut self) -> Result<()> {
use std::cell::RefCell;
use std::rc::Rc;
if let Some(msg) = check_apt_lock() {
self.status_message = msg;
self.state = AppState::Listing;
return Ok(());
}
self.state = AppState::Upgrading;
let progress_state = Rc::new(RefCell::new(
ProgressState::new("Applying Changes")?,
));
let acq = TuiAcquireProgress::new(Rc::clone(&progress_state));
let inst = TuiInstallProgress::new(Rc::clone(&progress_state));
let mut acquire_progress = rust_apt::progress::AcquireProgress::new(acq);
let mut install_progress = rust_apt::progress::InstallProgress::new(inst);
let config = rust_apt::config::Config::new();
config.set_vector("Dpkg::Options", &vec!["--force-confdef", "--force-confold"]);
unsafe { std::env::set_var("DEBIAN_FRONTEND", "noninteractive"); }
let redirect = StdioRedirect::capture()?;
let result = self.core.commit_with_progress(&mut acquire_progress, &mut install_progress);
self.output_lines = redirect.output();
self.output_scroll = 0;
match result {
Ok(()) => {
self.state = AppState::Done;
self.status_message = "Changes applied successfully. Space to continue.".to_string();
}
Err(e) => {
self.state = AppState::Done;
self.status_message = format!("Error: {e}. Space to continue.");
}
}
Ok(())
}
pub fn update_packages_live(&mut self) -> Result<()> {
use std::cell::RefCell;
use std::rc::Rc;
if let Some(msg) = check_apt_lock() {
self.status_message = msg;
return Ok(());
}
let progress_state = Rc::new(RefCell::new(
ProgressState::new("Updating Package Lists")?,
));
let acq = TuiAcquireProgress::new(Rc::clone(&progress_state));
let mut acquire_progress = rust_apt::progress::AcquireProgress::new(acq);
match self.core.update_with_progress(&mut acquire_progress) {
Ok(()) => {
self.core.rebuild_list();
self.core.update_cache_counts();
self.apply_current_filter();
self.update_status_message();
}
Err(e) => {
self.status_message = format!("Update failed: {e}");
}
}
Ok(())
}
pub fn refresh_cache(&mut self) -> Result<()> {
if let Some(msg) = check_apt_lock() {
self.status_message = msg;
return Ok(());
}
if let Err(e) = self.core.refresh() {
self.status_message = format!("Refresh failed: {e}");
return Ok(());
}
self.refresh_ui_state();
self.update_status_message();
Ok(())
}
}
fn clamped_scroll(current: usize, delta: i32, max: usize) -> usize {
(current as i32 + delta).clamp(0, max as i32) as usize
}