diff --git a/.gitignore b/.gitignore index 8e591ec..27eb4ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ CLAUDE.md notes.md /target/ +/docs/plans/ /testing/smoke-test.log diff --git a/Cargo.lock b/Cargo.lock index e51bbe4..5e6bd39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,17 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -165,6 +176,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cfg-if" version = "1.0.4" @@ -242,6 +259,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "conpty" version = "0.5.1" @@ -283,6 +310,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -919,6 +956,28 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "js-sys" version = "0.3.83" @@ -1040,10 +1099,10 @@ dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -1146,6 +1205,12 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "openssl-sys" version = "0.9.111" @@ -1179,8 +1244,10 @@ dependencies = [ "libc", "nix 0.29.0", "onefetch-image", - "reqwest", + "raur", + "reqwest 0.12.26", "serde", + "tokio", "toml", ] @@ -1307,6 +1374,19 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "raur" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb4cd4ad5a2a09f5ad0b45a7e986ab1468058c5006a9c92145725daac4859652" +dependencies = [ + "async-trait", + "reqwest 0.13.2", + "serde", + "serde_derive", + "serde_urlencoded", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1398,6 +1478,49 @@ dependencies = [ "web-sys", ] +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "ring" version = "0.17.14" @@ -1447,6 +1570,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe 0.2.1", + "rustls-pki-types", + "schannel", + "security-framework 3.5.1", +] + [[package]] name = "rustls-pki-types" version = "1.13.1" @@ -1456,6 +1591,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.5.1", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.8" @@ -1479,6 +1641,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "schannel" version = "0.1.28" @@ -1501,7 +1672,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -1707,7 +1891,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.10.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -1976,6 +2160,16 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "want" version = "0.3.1" @@ -2078,6 +2272,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2094,6 +2297,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2179,6 +2391,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index af51cf2..c371b3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,10 @@ expectrl = "0.7" indicatif = "0.17" libc = "0.2" nix = { version = "0.29", features = ["fs"] } +raur = "8" reqwest = { version = "0.12", features = ["blocking"] } serde = { version = "1", features = ["derive"] } +tokio = { version = "1", features = ["rt"] } toml = "0.8" onefetch-image = "2" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } diff --git a/default_config.toml b/default_config.toml index 418acfb..e702bf8 100644 --- a/default_config.toml +++ b/default_config.toml @@ -1,3 +1,8 @@ +# Default CLI arguments when pacfetch is run with no args. +# Examples: "--yay", "-Syu" +# Uncomment the line below to set a default: +# default_args = "--yay" + ################### DISPLAY #################### [display] ## ascii options: "PACMAN_DEFAULT", "PACMAN_SMALL", "NONE", or a file path diff --git a/src/config.rs b/src/config.rs index e97b405..46a2215 100644 --- a/src/config.rs +++ b/src/config.rs @@ -34,8 +34,10 @@ pub enum TitleAlign { Right, } -#[derive(Deserialize, Default)] +#[derive(Deserialize, Default, Clone)] pub struct Config { + #[serde(default)] + pub default_args: String, #[serde(default)] pub display: DisplayConfig, #[serde(default)] @@ -44,7 +46,7 @@ pub struct Config { pub disk: DiskConfig, } -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct CacheConfig { #[serde(default = "default_ttl")] pub ttl_minutes: u32, @@ -62,7 +64,7 @@ impl Default for CacheConfig { } } -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct DiskConfig { #[serde(default = "default_disk_path")] pub path: String, @@ -80,7 +82,7 @@ impl Default for DiskConfig { } } -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct PaletteConfig { #[serde(default = "default_palette_style")] pub style: String, @@ -105,7 +107,7 @@ impl Default for PaletteConfig { } } -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct GlyphConfig { #[serde(default = "default_glyph")] pub glyph: String, @@ -225,7 +227,7 @@ impl Default for TitleConfig { } } -#[derive(Deserialize)] +#[derive(Deserialize, Clone)] pub struct DisplayConfig { #[serde(default = "default_stats")] pub stats: Vec, diff --git a/src/main.rs b/src/main.rs index 16d4ae6..b2a50f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -33,6 +33,7 @@ Commands: -Sy Sync package databases -Su Upgrade system -Syu Sync databases and upgrade system + --yay Full system + AUR upgrade via yay Options: --ascii Use custom ASCII art (path, built-in name, or NONE) @@ -66,6 +67,13 @@ struct Cli { #[arg(long = "ascii", hide = true)] ascii: Option, + + #[arg(long = "yay", hide = true)] + yay: bool, +} + +fn is_bare_invocation(cli: &Cli) -> bool { + !cli.sync_op && !cli.sync_db && !cli.upgrade && !cli.yay && !cli.local } fn print_error_and_help(msg: &str) -> ! { @@ -98,10 +106,36 @@ fn main() { let mut config = Config::load(); // Override ascii from CLI - if let Some(ascii) = cli.ascii { - config.display.ascii = ascii; + if let Some(ref ascii) = cli.ascii { + config.display.ascii = ascii.clone(); } + // Apply default_args if no meaningful flags were provided + let cli = if is_bare_invocation(&cli) && !config.default_args.is_empty() { + let mut args = vec!["pacfetch".to_string()]; + args.extend(config.default_args.split_whitespace().map(String::from)); + // Carry forward any explicit flags that were set + if cli.debug { + args.push("-d".to_string()); + } + if let Some(ref ascii) = cli.ascii { + args.push("--ascii".to_string()); + args.push(ascii.clone()); + } + match Cli::try_parse_from(&args) { + Ok(cli) => cli, + Err(_) => { + eprintln!( + "warning: invalid default_args in config: {:?}", + config.default_args + ); + cli + } + } + } else { + cli + }; + let invalid_flag = (cli.sync_op && !cli.sync_db && !cli.upgrade) || ((cli.sync_db || cli.upgrade) && !cli.sync_op); if invalid_flag { @@ -118,6 +152,15 @@ fn main() { std::process::exit(0); } + // Handle --yay (full system + AUR upgrade via yay) + if cli.yay { + if let Err(e) = pacman::yay_upgrade(cli.debug, &config) { + eprintln!("error: {}", e); + std::process::exit(1); + } + std::process::exit(0); + } + // Skip fresh sync if: --local flag, or after -Sy let fresh_sync = !(cli.local || cli.sync_op && cli.sync_db); diff --git a/src/pacman.rs b/src/pacman.rs index 2d8a193..18970ae 100644 --- a/src/pacman.rs +++ b/src/pacman.rs @@ -6,6 +6,7 @@ use crate::util; use alpm::Alpm; use chrono::{DateTime, FixedOffset, Local}; use indicatif::{ProgressBar, ProgressStyle}; +use raur::Raur as _; use std::fs; use std::os::unix::fs::symlink; use std::path::PathBuf; @@ -843,6 +844,63 @@ fn run_pacman_pty(args: &[&str], filter: bool) -> Result<(), String> { Ok(()) } +fn get_aur_upgradable_count() -> u32 { + let mut handle = match Alpm::new("/", "/var/lib/pacman") { + Ok(a) => a, + Err(_) => return 0, + }; + let _ = handle.register_syncdb_mut("core", alpm::SigLevel::NONE); + let _ = handle.register_syncdb_mut("extra", alpm::SigLevel::NONE); + let _ = handle.register_syncdb_mut("multilib", alpm::SigLevel::NONE); + + let mut repo_pkgs: std::collections::HashSet = std::collections::HashSet::new(); + for db in handle.syncdbs().into_iter() { + for pkg in db.pkgs().into_iter() { + repo_pkgs.insert(pkg.name().to_string()); + } + } + + let foreign: Vec<(String, String)> = handle + .localdb() + .pkgs() + .into_iter() + .filter(|pkg| !repo_pkgs.contains(pkg.name())) + .map(|pkg| (pkg.name().to_string(), pkg.version().to_string())) + .collect(); + + if foreign.is_empty() { + return 0; + } + + let pkg_names: Vec<&str> = foreign.iter().map(|(n, _)| n.as_str()).collect(); + let Ok(rt) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + else { + return 0; + }; + let Ok(aur_pkgs): Result, _> = + rt.block_on(async { raur::Handle::new().info(&pkg_names).await }) + else { + return 0; + }; + + let installed: std::collections::HashMap<&str, &str> = foreign + .iter() + .map(|(n, v)| (n.as_str(), v.as_str())) + .collect(); + + aur_pkgs + .iter() + .filter(|p| { + installed + .get(p.name.as_str()) + .map(|iv| alpm::vercmp(p.version.as_str(), iv) == std::cmp::Ordering::Greater) + .unwrap_or(false) + }) + .count() as u32 +} + fn run_pacman_sync() -> Result<(), String> { if !util::is_root() { return Err("you cannot perform this operation unless you are root.".to_string()); @@ -920,6 +978,40 @@ pub fn sync_databases() -> Result<(), String> { run_pacman_sync() } +pub fn yay_upgrade(debug: bool, config: &crate::config::Config) -> Result<(), String> { + if !matches!(Command::new("yay").arg("--version").output(), Ok(o) if o.status.success()) { + return Err("yay is not installed.".to_string()); + } + + // Sync temp databases + let spinner = if debug { + None + } else { + Some(util::create_spinner("Gathering stats")) + }; + let stat_ids = config.display.parsed_stats(); + let mut stats = get_stats(&stat_ids, debug, true, config, spinner.as_ref()); + let aur_count = get_aur_upgradable_count(); + if let Some(ref s) = spinner { + s.finish_and_clear(); + } + + stats.total_upgradable += aur_count; + + if debug { + crate::ui::display_stats(&stats, config); + println!(); + } else if let Err(e) = crate::ui::display_stats_with_graphics(&stats, config) { + eprintln!("error: {}", e); + crate::ui::display_stats(&stats, config); + println!(); + } + + //hand off to yay + use std::os::unix::process::CommandExt; + Err(std::process::Command::new("yay").exec().to_string()) +} + pub fn upgrade_system( debug: bool, sync_first: bool, diff --git a/src/util.rs b/src/util.rs index 80e1305..6bd4f88 100644 --- a/src/util.rs +++ b/src/util.rs @@ -46,19 +46,42 @@ pub fn create_spinner(message: &str) -> ProgressBar { pb } -/// Strip ANSI escape codes from a string pub fn strip_ansi(s: &str) -> String { let mut result = String::new(); - let mut in_escape = false; - for c in s.chars() { - if c == '\x1b' { - in_escape = true; - } else if in_escape { - if c == 'm' { - in_escape = false; - } - } else { + let mut chars = s.chars().peekable(); + + while let Some(c) = chars.next() { + if c != '\x1b' { result.push(c); + continue; + } + match chars.peek().copied() { + Some('[') => { + chars.next(); + for nc in chars.by_ref() { + if ('@'..='~').contains(&nc) { + break; + } + } + } + Some(']') => { + chars.next(); + while let Some(nc) = chars.next() { + if nc == '\x07' { + break; + } + if nc == '\x1b' { + if chars.peek() == Some(&'\\') { + chars.next(); + } + break; + } + } + } + Some(_) => { + chars.next(); + } + None => {} } } result