From 456453bc174930d7cb46012075efa30b2b427cb9 Mon Sep 17 00:00:00 2001 From: camtisocial Date: Tue, 10 Feb 2026 20:36:55 -0800 Subject: [PATCH 1/3] 1.1.0 --- Cargo.lock | 172 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- 2 files changed, 174 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7cc2b45..9ccc22b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + [[package]] name = "aho-corasick" version = "1.1.4" @@ -89,6 +95,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "anyhow" +version = "1.0.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" + [[package]] name = "atomic-waker" version = "1.1.2" @@ -125,6 +137,18 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.0" @@ -206,6 +230,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colorchoice" version = "1.0.4" @@ -259,6 +289,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossterm" version = "0.29.0" @@ -398,12 +437,31 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -783,6 +841,33 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + [[package]] name = "indexmap" version = "2.12.1" @@ -914,6 +999,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.1.1" @@ -926,6 +1021,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "native-tls" version = "0.2.14" @@ -995,6 +1100,20 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "onefetch-image" +version = "2.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28da8c1e4e78991d772dbf90316ef2262730dbfbdfa80fac3ac02cf346794150" +dependencies = [ + "anyhow", + "base64", + "clap", + "color_quant", + "image", + "rustix", +] + [[package]] name = "openssl" version = "0.10.75" @@ -1047,7 +1166,7 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "pacfetch" -version = "1.0.0" +version = "1.1.0" dependencies = [ "alpm", "chrono", @@ -1055,9 +1174,11 @@ dependencies = [ "crossterm", "dirs", "expectrl", + "image", "indicatif", "libc", "nix 0.29.0", + "onefetch-image", "reqwest", "serde", "toml", @@ -1110,6 +1231,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1143,6 +1277,21 @@ dependencies = [ "nix 0.26.4", ] +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + [[package]] name = "quote" version = "1.0.42" @@ -1474,6 +1623,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + [[package]] name = "slab" version = "0.4.11" @@ -2415,3 +2570,18 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index d63998d..686317d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pacfetch" -version = "1.0.0" +version = "1.1.0" edition = "2024" authors = ["camtisocial thompsonca99@gmail.com"] license = "GPL-3.0" @@ -28,3 +28,5 @@ nix = { version = "0.29", features = ["fs"] } reqwest = { version = "0.12", features = ["blocking"] } serde = { version = "1", features = ["derive"] } toml = "0.8" +onefetch-image = "2" +image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp"] } From 88c6fd7f5ea2a709c9e086b9a08068a550ab6272 Mon Sep 17 00:00:00 2001 From: camtisocial Date: Tue, 10 Feb 2026 20:38:00 -0800 Subject: [PATCH 2/3] added image support --- default_config.toml | 3 ++ src/config.rs | 4 ++ src/ui/ascii.rs | 14 +------ src/ui/mod.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++ src/util.rs | 14 +++++++ 5 files changed, 123 insertions(+), 12 deletions(-) diff --git a/default_config.toml b/default_config.toml index ca3e009..9d8681a 100644 --- a/default_config.toml +++ b/default_config.toml @@ -4,6 +4,9 @@ ascii = "PACMAN_DEFAULT" ascii_color = "yellow" +# Image takes precedence when set +# image = "~/.config/pacfetch/logo.png" + # Available stats: installed, upgradable, last_update, download_size, installed_size, # net_upgrade_size, orphaned_packages, cache_size, disk, mirror_url, mirror_health stats = [ diff --git a/src/config.rs b/src/config.rs index 1feebce..7ac0895 100644 --- a/src/config.rs +++ b/src/config.rs @@ -199,6 +199,9 @@ pub struct DisplayConfig { #[serde(default = "default_ascii_color")] pub ascii_color: String, + #[serde(default)] + pub image: String, + #[serde(default)] pub glyph: GlyphConfig, @@ -246,6 +249,7 @@ impl Default for DisplayConfig { stats: default_stats(), ascii: default_ascii(), ascii_color: default_ascii_color(), + image: String::new(), glyph: GlyphConfig::default(), colors: ColorsConfig::default(), labels: HashMap::new(), diff --git a/src/ui/ascii.rs b/src/ui/ascii.rs index 8865350..4296ab0 100644 --- a/src/ui/ascii.rs +++ b/src/ui/ascii.rs @@ -1,5 +1,5 @@ use std::fs; -use std::path::{Path, PathBuf}; +use std::path::Path; use crate::util; @@ -48,17 +48,7 @@ fn normalize_width(lines: Vec) -> Vec { } fn load_from_file(path: &str) -> Vec { - let expanded = if path.starts_with('~') { - // When running via sudo, use the original user's home - let home = if let Ok(sudo_user) = std::env::var("SUDO_USER") { - PathBuf::from(format!("/home/{}", sudo_user)) - } else { - dirs::home_dir().unwrap_or_default() - }; - path.replacen('~', &home.to_string_lossy(), 1) - } else { - path.to_string() - }; + let expanded = crate::util::expand_path(path); let path = Path::new(&expanded); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 8a5ef99..7fb2edd 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -236,6 +236,88 @@ pub fn display_stats(stats: &PacmanStats, config: &Config) { } } +fn try_render_with_image(stats_lines: Vec, config: &Config) -> Option { + let image_path = &config.display.image; + if image_path.is_empty() { + return None; + } + + let expanded = crate::util::expand_path(image_path); + let img = match image::ImageReader::open(&expanded) + .and_then(|r| r.with_guessed_format()) + .map_err(image::ImageError::IoError) + .and_then(|r| r.decode()) + { + Ok(img) => img, + Err(e) => { + crate::log::warn(&format!("Failed to load image '{}': {}", expanded, e)); + return None; + } + }; + + let backend = match onefetch_image::get_best_backend() { + Ok(Some(b)) => b, + Ok(None) => { + crate::log::warn("No supported image protocol detected, falling back to ASCII"); + return None; + } + Err(e) => { + crate::log::warn(&format!("Failed to detect image backend: {}", e)); + return None; + } + }; + + match backend.add_image(stats_lines, &img, 256) { + Ok(output) => Some(fix_image_cursor_up(output)), + Err(e) => { + crate::log::warn(&format!("Image rendering failed: {}, falling back to ASCII", e)); + None + } + } +} + +/// Fix vertical misalignment between image and text overlay. +fn fix_image_cursor_up(output: String) -> String { + let extra: u32 = if output.contains("\x1b_G") { + if std::env::var("TERM_PROGRAM").is_ok_and(|v| v == "ghostty") { + 2 + } else { + 1 + } + } else if output.contains("\x1b]1337") { + 1 + } else { + return output; + }; + + let bytes = output.as_bytes(); + let mut i = 0; + while i + 2 < bytes.len() { + if bytes[i] == 0x1b && bytes[i + 1] == b'[' { + let start = i + 2; + let mut end = start; + while end < bytes.len() && bytes[end].is_ascii_digit() { + end += 1; + } + if end > start && end < bytes.len() && bytes[end] == b'A' { + if let Some(n) = std::str::from_utf8(&bytes[start..end]) + .ok() + .and_then(|s| s.parse::().ok()) + { + return format!( + "{}\x1b[{}A{}", + &output[..i], + n + extra, + &output[end + 1..] + ); + } + } + } + i += 1; + } + output +} + pub fn display_stats_with_graphics(stats: &PacmanStats, config: &Config) -> io::Result<()> { let ascii_art = ascii::get_art(&config.display.ascii); let ascii_color = parse_color(&config.display.ascii_color); @@ -372,6 +454,24 @@ pub fn display_stats_with_graphics(stats: &PacmanStats, config: &Config) -> io:: stats_lines.push(color_row_2); } + // Try image rendering first, fall back to ASCII art + if !config.display.image.is_empty() { + // Pad stats lines with leading spaces to match ASCII art spacing (3-space gap) + let padded_lines: Vec = stats_lines + .iter() + .map(|line| format!(" {}", line)) + .collect(); + if let Some(output) = try_render_with_image(padded_lines, config) { + println!(); + // Indent each line for left margin (1 space, matching ASCII path) + for line in output.lines() { + println!(" {}", line); + } + println!(); + return Ok(()); + } + } + // Print with ASCII art println!(); if ascii_art.is_empty() { diff --git a/src/util.rs b/src/util.rs index 378af56..80e1305 100644 --- a/src/util.rs +++ b/src/util.rs @@ -64,6 +64,20 @@ pub fn strip_ansi(s: &str) -> String { result } +/// Expand ~ to the user's home directory, respecting SUDO_USER +pub fn expand_path(path: &str) -> String { + if path.starts_with('~') { + let home = if let Ok(sudo_user) = std::env::var("SUDO_USER") { + PathBuf::from(format!("/home/{}", sudo_user)) + } else { + dirs::home_dir().unwrap_or_default() + }; + path.replacen('~', &home.to_string_lossy(), 1) + } else { + path.to_string() + } +} + /// Check if running as root pub fn is_root() -> bool { #[cfg(unix)] From 461906ec19efab052af2449f03400c293102f8e5 Mon Sep 17 00:00:00 2001 From: camtisocial Date: Tue, 10 Feb 2026 20:38:51 -0800 Subject: [PATCH 3/3] fmt --- src/ui/mod.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 7fb2edd..fd7bdc2 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -270,7 +270,10 @@ fn try_render_with_image(stats_lines: Vec, config: &Config) -> Option Some(fix_image_cursor_up(output)), Err(e) => { - crate::log::warn(&format!("Image rendering failed: {}, falling back to ASCII", e)); + crate::log::warn(&format!( + "Image rendering failed: {}, falling back to ASCII", + e + )); None } } @@ -285,7 +288,7 @@ fn fix_image_cursor_up(output: String) -> String { 1 } } else if output.contains("\x1b]1337") { - 1 + 1 } else { return output; }; @@ -299,18 +302,14 @@ fn fix_image_cursor_up(output: String) -> String { while end < bytes.len() && bytes[end].is_ascii_digit() { end += 1; } - if end > start && end < bytes.len() && bytes[end] == b'A' { - if let Some(n) = std::str::from_utf8(&bytes[start..end]) + if end > start + && end < bytes.len() + && bytes[end] == b'A' + && let Some(n) = std::str::from_utf8(&bytes[start..end]) .ok() .and_then(|s| s.parse::().ok()) - { - return format!( - "{}\x1b[{}A{}", - &output[..i], - n + extra, - &output[end + 1..] - ); - } + { + return format!("{}\x1b[{}A{}", &output[..i], n + extra, &output[end + 1..]); } } i += 1;