mod apt;
mod cli;
mod config;
mod desktop;
mod error;
mod history;
mod kernel;
mod sources;
mod ui;
mod updater;
use anyhow::Result;
use clap::Parser;
use std::sync::mpsc;
use std::thread;
use cli::{Cli, Commands, DesktopCommands, HistoryCommands, KernelCommands, SourcesCommands, VanillaCommands};
use config::Config;
use ui::should_use_tui;
fn main() -> Result<()> {
ensure_root();
let cli = Cli::parse();
let cfg = Config::load().unwrap_or_default();
let update_url = cfg.updates.check_url.clone();
let check_enabled = cfg.updates.check_enabled && !cli.no_update_check;
let update_thread = if check_enabled {
let (tx, rx) = mpsc::channel();
let url = update_url.clone();
thread::spawn(move || {
let info = updater::check_for_update(&url);
let _ = tx.send(info);
});
Some(rx)
} else {
None
};
let use_tui = should_use_tui(cli.no_tui);
let result = match cli.command {
None => {
if !use_tui {
anyhow::bail!("rustpm requires a terminal. Run with --no-tui for plain output, or use a subcommand.");
}
run_tui(&cfg)
}
Some(cmd) => run_command(cmd, use_tui, &cfg),
};
if let Some(rx) = update_thread {
if let Ok(Some(info)) = rx.try_recv() {
eprintln!(
"\n rustpm {} is available (you have {}). See: {}",
info.latest, info.current, info.url
);
}
}
result
}
fn run_tui(cfg: &Config) -> Result<()> {
use ui::tui::run_tui;
use ui::tui::app::App;
let packages = apt::query::list_packages(true, false).unwrap_or_default();
let upgrade_changes = apt::executor::dry_run(&apt::executor::AptOperation::Upgrade { full: false }).unwrap_or_default();
let kernels = kernel::detect_kernels().unwrap_or_default();
let vanilla = kernel::vanilla::fetch_releases().unwrap_or_default();
let desktops = desktop::manager::list_desktops();
let sources = sources::parse_all_sources().unwrap_or_default();
let hist = history::list_history(cfg.history_max_entries).unwrap_or_default();
let app = App::new(packages, vec![], upgrade_changes, kernels, vanilla, desktops, sources, hist);
run_tui(app)
}
fn run_command(cmd: Commands, use_tui: bool, cfg: &Config) -> Result<()> {
match cmd {
Commands::Update => {
let (tx, rx) = mpsc::channel();
let op = apt::executor::AptOperation::Update;
if use_tui {
let status = apt::executor::execute(&op, Some(tx))?;
for line in rx { println!("{}", line); }
if !status.success() {
anyhow::bail!("apt-get update failed");
}
} else {
apt::executor::execute(&op, None)?;
}
}
Commands::Upgrade { full } => {
let op = apt::executor::AptOperation::Upgrade { full };
let changes = apt::executor::dry_run(&op)?;
if changes.is_empty() {
println!("Nothing to upgrade.");
return Ok(());
}
if use_tui {
print_changes_tui(&changes, &op)?;
} else {
print_changes_plain(&changes);
if confirm("Proceed with upgrade?")? {
apt::executor::execute(&op, None)?;
}
}
}
Commands::Install { packages } => {
if packages.is_empty() {
anyhow::bail!("No packages specified");
}
let op = apt::executor::AptOperation::Install(packages);
let changes = apt::executor::dry_run(&op)?;
if use_tui {
print_changes_tui(&changes, &op)?;
} else {
print_changes_plain(&changes);
if confirm("Proceed with install?")? {
apt::executor::execute(&op, None)?;
}
}
}
Commands::Remove { packages, purge } => {
if packages.is_empty() {
anyhow::bail!("No packages specified");
}
let op = if purge {
apt::executor::AptOperation::Purge(packages)
} else {
apt::executor::AptOperation::Remove(packages)
};
let changes = apt::executor::dry_run(&op)?;
if use_tui {
print_changes_tui(&changes, &op)?;
} else {
print_changes_plain(&changes);
if confirm("Proceed with removal?")? {
apt::executor::execute(&op, None)?;
}
}
}
Commands::Purge { packages } => {
if packages.is_empty() {
anyhow::bail!("No packages specified");
}
let op = apt::executor::AptOperation::Purge(packages);
let changes = apt::executor::dry_run(&op)?;
if use_tui {
print_changes_tui(&changes, &op)?;
} else {
print_changes_plain(&changes);
if confirm("Proceed with purge?")? {
apt::executor::execute(&op, None)?;
}
}
}
Commands::Search { term, names_only } => {
let results = apt::query::search_packages(&term, names_only)?;
if results.is_empty() {
println!("No packages found for '{}'", term);
return Ok(());
}
use ui::table::{Column, Row, print_table};
use ui::colors::{header_style, install_style};
let cols = vec![
Column::new("Package").with_style(header_style()),
Column::new("Description"),
];
let rows: Vec<_> = results
.iter()
.map(|p| {
let style = if p.installed_version.is_some() {
Some(install_style())
} else {
None
};
let mut r = Row::new(vec![
p.name.clone(),
p.description.as_deref().unwrap_or("").to_string(),
]);
if let Some(s) = style { r = r.with_style(s); }
r
})
.collect();
print_table(&cols, &rows);
}
Commands::Show { package } => {
let info = apt::query::show_package(&package)?;
println!("Package: {}", info.name);
if let Some(v) = &info.installed_version {
println!("Installed: {}", v);
}
if let Some(v) = &info.candidate_version {
println!("Candidate: {}", v);
}
if let Some(s) = info.installed_size_kb {
println!("Installed-Size: {} kB", s);
}
if let Some(s) = &info.section {
println!("Section: {}", s);
}
if let Some(h) = &info.homepage {
println!("Homepage: {}", h);
}
if !info.depends.is_empty() {
println!("Depends: {}", info.depends.join(", "));
}
if let Some(d) = &info.description {
println!("\n{}", d);
}
}
Commands::List { installed, upgradable } => {
let packages = apt::query::list_packages(installed, upgradable)?;
use ui::table::{Column, Row, print_table};
use ui::colors::header_style;
let cols = vec![
Column::new("Package").with_style(header_style()),
Column::new("Installed").with_style(header_style()),
Column::new("Available").with_style(header_style()),
];
let rows: Vec<_> = packages
.iter()
.map(|p| {
Row::new(vec![
p.name.clone(),
p.installed_version.as_deref().unwrap_or("—").to_string(),
p.candidate_version.as_deref().unwrap_or("—").to_string(),
])
})
.collect();
print_table(&cols, &rows);
}
Commands::History { action, limit } => {
match action {
None => {
let entries = history::list_history(limit)?;
for e in &entries {
let ts = &e.timestamp;
let pkgs: String = e
.packages
.iter()
.take(3)
.map(|p| p.name.clone())
.collect::<Vec<_>>()
.join(", ");
println!("#{:3} {} {:12} {}", e.id, ts, e.operation, pkgs);
}
}
Some(HistoryCommands::Undo { id }) => {
let entry = history::get_entry(id)?
.ok_or_else(|| anyhow::anyhow!("History entry #{} not found", id))?;
println!("Undoing #{}: {} ...", id, entry.operation);
for item in &entry.packages {
if let Some(ref old_ver) = item.old_version {
let pkg_ver = format!("{}={}", item.name, old_ver);
let op = apt::executor::AptOperation::Install(vec![pkg_ver]);
apt::executor::execute(&op, None)?;
} else if item.old_version.is_none() {
let op = apt::executor::AptOperation::Remove(vec![item.name.clone()]);
apt::executor::execute(&op, None)?;
}
}
}
}
}
Commands::Kernel { action } => run_kernel_command(action, cfg)?,
Commands::Desktop { action } => run_desktop_command(action)?,
Commands::Sources { action } => run_sources_command(action)?,
}
Ok(())
}
fn run_kernel_command(action: KernelCommands, _cfg: &Config) -> Result<()> {
match action {
KernelCommands::List => {
let kernels = kernel::detect_kernels()?;
use ui::table::{Column, Row, print_table};
use ui::colors::{header_style, running_kernel_style, held_style};
let cols = vec![
Column::new("Version").with_style(header_style()),
Column::new("Image").with_style(header_style()),
Column::new("Headers").with_style(header_style()),
Column::new("APT Version").with_style(header_style()),
Column::new("Hold").with_style(header_style()),
];
let rows: Vec<_> = kernels
.iter()
.map(|k| {
let ver = if k.is_running {
format!("* {}", k.version)
} else {
format!(" {}", k.version)
};
let style = if k.is_running {
Some(running_kernel_style())
} else if k.is_held {
Some(held_style())
} else {
None
};
let mut r = Row::new(vec![
ver,
if k.installed { "installed".into() } else { "—".into() },
if k.headers_installed { "installed".into() } else { "—".into() },
k.apt_version.as_deref().unwrap_or("—").to_string(),
if k.is_held { "[held]".into() } else { "".into() },
]);
if let Some(s) = style { r = r.with_style(s); }
r
})
.collect();
print_table(&cols, &rows);
}
KernelCommands::Install { version } => {
let (tx, rx) = mpsc::channel();
let tx_clone = tx.clone();
let v = version.clone();
let handle = thread::spawn(move || {
kernel::manager::install_kernel(&v, &tx_clone)
});
for line in rx { println!("{}", line); }
handle.join().unwrap()?;
}
KernelCommands::Remove { version } => {
let (tx, rx) = mpsc::channel();
let tx_clone = tx.clone();
let v = version.clone();
let handle = thread::spawn(move || {
kernel::manager::remove_kernel(&v, &tx_clone)
});
for line in rx { println!("{}", line); }
handle.join().unwrap()?;
}
KernelCommands::Update => {
let (tx, rx) = mpsc::channel();
let tx_clone = tx.clone();
let handle = thread::spawn(move || {
kernel::manager::update_kernel(&tx_clone)
});
for line in rx { println!("{}", line); }
handle.join().unwrap()?;
}
KernelCommands::Pin { version } => {
kernel::manager::pin_kernel(&version)?;
println!("Pinned kernel {}", version);
}
KernelCommands::Unpin { version } => {
kernel::manager::unpin_kernel(&version)?;
println!("Unpinned kernel {}", version);
}
KernelCommands::Vanilla { action } => run_vanilla_command(action)?,
}
Ok(())
}
fn run_vanilla_command(action: VanillaCommands) -> Result<()> {
match action {
VanillaCommands::List { stable, mainline, longterm } => {
let releases = kernel::vanilla::fetch_releases()?;
use kernel::vanilla::ReleaseType;
use ui::table::{Column, Row, print_table};
use ui::colors::header_style;
let filtered: Vec<_> = releases
.iter()
.filter(|r| {
if !stable && !mainline && !longterm {
r.release_type != ReleaseType::Eol
} else {
(stable && r.release_type == ReleaseType::Stable)
|| (mainline && r.release_type == ReleaseType::Mainline)
|| (longterm && r.release_type == ReleaseType::Longterm)
}
})
.collect();
let cols = vec![
Column::new("Version").with_style(header_style()),
Column::new("Type").with_style(header_style()),
Column::new("Released").with_style(header_style()),
Column::new("Installed").with_style(header_style()),
];
let rows: Vec<_> = filtered
.iter()
.map(|r| {
let released = r.released.as_deref().unwrap_or("—").to_string();
Row::new(vec![
r.version.clone(),
r.release_type.label().to_string(),
released,
if r.installed { "✓".into() } else { "".into() },
])
})
.collect();
print_table(&cols, &rows);
}
VanillaCommands::Install { version } => {
let (tx, rx) = mpsc::channel();
let tx_clone = tx.clone();
let v = version.clone();
let handle = thread::spawn(move || {
kernel::vanilla::install_vanilla(&v, &tx_clone)
});
for line in rx { println!("{}", line); }
handle.join().unwrap()?;
}
VanillaCommands::Remove { version } => {
let (tx, rx) = mpsc::channel();
let tx_clone = tx.clone();
let v = version.clone();
let handle = thread::spawn(move || {
kernel::vanilla::remove_vanilla(&v, &tx_clone)
});
for line in rx { println!("{}", line); }
handle.join().unwrap()?;
}
VanillaCommands::Info { version } => {
let releases = kernel::vanilla::fetch_releases()?;
if let Some(r) = releases.iter().find(|r| r.version == version) {
println!("Version: {}", r.version);
println!("Type: {}", r.release_type.label());
if let Some(d) = &r.released {
println!("Released: {}", d);
}
if let Some(d) = &r.eol {
println!("EOL: {}", d);
}
if !r.changelog_url.is_empty() {
println!("Changelog: {}", r.changelog_url);
}
println!("Installed: {}", if r.installed { "yes" } else { "no" });
} else {
anyhow::bail!("Version {} not found in kernel.org release list", version);
}
}
}
Ok(())
}
fn run_desktop_command(action: DesktopCommands) -> Result<()> {
match action {
DesktopCommands::List => {
let desktops = desktop::manager::list_desktops();
use ui::table::{Column, Row, print_table};
use ui::colors::{header_style, install_style};
let cols = vec![
Column::new("").with_style(header_style()),
Column::new("Desktop").with_style(header_style()),
Column::new("Packages").with_style(header_style()),
Column::new("Display Manager").with_style(header_style()),
];
let rows: Vec<_> = desktops
.iter()
.map(|d| {
let check = if d.installed { "✓" } else { " " };
let pkgs = d.profile.packages.join(", ");
let mut r = Row::new(vec![
check.to_string(),
d.profile.display_name.to_string(),
pkgs,
d.profile.display_manager.to_string(),
]);
if d.installed { r = r.with_style(install_style()); }
r
})
.collect();
print_table(&cols, &rows);
}
DesktopCommands::Install { name } => {
desktop::manager::install_desktop(&name)?;
println!("Installed desktop environment: {}", name);
}
DesktopCommands::Remove { name } => {
desktop::manager::remove_desktop(&name)?;
println!("Removed desktop environment: {}", name);
}
DesktopCommands::Switch { name } => {
desktop::manager::switch_desktop(&name)?;
}
}
Ok(())
}
fn run_sources_command(action: SourcesCommands) -> Result<()> {
match action {
SourcesCommands::List => {
let sources = sources::parse_all_sources()?;
use ui::table::{Column, Row, print_table};
use ui::colors::{header_style, dim_style};
let cols = vec![
Column::new("").with_style(header_style()),
Column::new("Type").with_style(header_style()),
Column::new("URI").with_style(header_style()),
Column::new("Suite").with_style(header_style()),
Column::new("Components").with_style(header_style()),
];
let rows: Vec<_> = sources
.iter()
.map(|s| {
let check = if s.enabled { "✓" } else { "✗" };
let style = if !s.enabled { Some(dim_style()) } else { None };
let mut r = Row::new(vec![
check.to_string(),
s.source_type.clone(),
s.uri.clone(),
s.suite.clone(),
s.components.join(" "),
]);
if let Some(st) = style { r = r.with_style(st); }
r
})
.collect();
print_table(&cols, &rows);
}
SourcesCommands::Add { uri, suite, components } => {
sources::manager::add_source(&uri, &suite, &components)?;
}
SourcesCommands::Remove { uri } => {
sources::manager::remove_source(&uri)?;
}
SourcesCommands::Enable { uri } => {
sources::manager::enable_source(&uri)?;
}
SourcesCommands::Disable { uri } => {
sources::manager::disable_source(&uri)?;
}
SourcesCommands::Edit => {
sources::manager::edit_sources()?;
}
}
Ok(())
}
fn print_changes_plain(changes: &[apt::parser::PackageChange]) {
use apt::parser::ChangeKind;
use nu_ansi_term::Color;
let installs: Vec<_> = changes.iter().filter(|c| c.kind == ChangeKind::Install).collect();
let removes: Vec<_> = changes.iter().filter(|c| c.kind == ChangeKind::Remove).collect();
let upgrades: Vec<_> = changes.iter().filter(|c| c.kind == ChangeKind::Upgrade).collect();
if !installs.is_empty() {
println!("{}", Color::Green.bold().paint("To install:"));
for c in &installs {
println!(" {} {}", c.name, c.new_version.as_deref().unwrap_or(""));
}
}
if !upgrades.is_empty() {
println!("{}", Color::Yellow.bold().paint("To upgrade:"));
for c in &upgrades {
println!(" {} {} → {}", c.name,
c.old_version.as_deref().unwrap_or("?"),
c.new_version.as_deref().unwrap_or("?"));
}
}
if !removes.is_empty() {
println!("{}", Color::Red.bold().paint("To remove:"));
for c in &removes {
println!(" {} {}", c.name, c.old_version.as_deref().unwrap_or(""));
}
}
}
fn print_changes_tui(
changes: &[apt::parser::PackageChange],
_op: &apt::executor::AptOperation,
) -> Result<()> {
use ui::tui::{run_tui, app::App};
use apt::query::list_packages;
let packages = list_packages(false, false).unwrap_or_default();
let app = App::new(
packages,
changes.to_vec(),
changes.to_vec(),
vec![],
vec![],
vec![],
vec![],
vec![],
);
run_tui(app)?;
Ok(())
}
fn confirm(prompt: &str) -> Result<bool> {
use std::io::{self, Write};
print!("{} [Y/n] ", prompt);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let trimmed = input.trim().to_lowercase();
Ok(trimmed.is_empty() || trimmed == "y" || trimmed == "yes")
}
fn ensure_root() {
let uid = std::process::Command::new("id")
.arg("-u")
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.map(|s| s.trim().to_string())
.unwrap_or_default();
if uid == "0" {
return;
}
let exe = std::env::current_exe()
.unwrap_or_else(|_| std::path::PathBuf::from("rustpm"));
let user_args: Vec<String> = std::env::args().skip(1).collect();
let status = std::process::Command::new("sudo")
.arg(exe)
.args(&user_args)
.status()
.unwrap_or_else(|e| {
eprintln!("rustpm: failed to run sudo: {}", e);
std::process::exit(1);
});
std::process::exit(status.code().unwrap_or(1));
}