From 63e331c22c435695ab6621285925565d1b6f30c1 Mon Sep 17 00:00:00 2001 From: Kushagra Kumar Date: Sat, 7 Nov 2026 03:52:30 +0530 Subject: [PATCH 1/4] feat(output): zero-dependency adaptive ascii logos and color fallbacks - Add responsive layout constraint solver in formatter: dynamically selects Side-by-Side (Full/Compact), Stacked (Full/Compact), or Info-Only based on column width and content constraints without terminal line-wrapping glitches. - Introduce handcrafted compact ASCII art variants across all 40 distribution logos in logo registry with backwards-compatible rendering. - Implement zero-heap 3-tier ANSI color degradation engine mapping TrueColor -> 256-color -> Basic 16-color (via Euclidean sRGB distance) -> None, preventing escape code leakage on Linux virtual consoles and serial consoles. - Upgrade terminal column probing cascade to evaluate $COLUMNS override, standard streams ioctl (stdout/stderr/stdin), and /dev/tty descriptor. - Support --logo-variant , --compact-logo, and config.toml logo_variant property across CLI, configuration, and Bash/Zsh/Fish completions. - Expand test coverage across color degradation, variant layouts, CLI flags, and completions (285 total tests passing). --- completions/_kkfetch | 3 + completions/kkfetch.bash | 6 +- completions/kkfetch.fish | 3 + src/cli.rs | 8 + src/config.rs | 18 ++ src/context.rs | 108 ++++++++-- src/main.rs | 12 +- src/output/color.rs | 164 +++++++++++++++ src/output/formatter.rs | 198 ++++++++++++++++-- src/output/logo.rs | 410 +++++++++++++++++++++++++++++++++++++- tests/cli_tests.rs | 58 +++++- tests/completion_tests.rs | 2 + 12 files changed, 946 insertions(+), 44 deletions(-) diff --git a/completions/_kkfetch b/completions/_kkfetch index 2dc5813..20b679e 100644 --- a/completions/_kkfetch +++ b/completions/_kkfetch @@ -95,6 +95,9 @@ _kkfetch() { '*--disable=[Disable specific modules (comma-separated)]:DISABLE:_values -s , "module" $modules' \ '-l+[Override the ASCII logo]:LOGO:_describe "logo" logos' \ '--logo=[Override the ASCII logo]:LOGO:_describe "logo" logos' \ + '--logo-variant=[Select ASCII logo variant]:VARIANT:(auto full compact)' \ + '--compact-logo[Force compact ASCII logo variant]' \ + '--compact[Force compact ASCII logo variant (alias)]' \ '--disk-path=[Target mount point or directory path for disk usage statistics (default\: "/")]:DISK_PATH:_directories' \ '--all-disks[Show all mounted filesystems and auxiliary partitions]' \ '--no-color[Disable colored ANSI output]' \ diff --git a/completions/kkfetch.bash b/completions/kkfetch.bash index 6ecd272..6bcf5ff 100644 --- a/completions/kkfetch.bash +++ b/completions/kkfetch.bash @@ -13,11 +13,15 @@ _kkfetch() { prev="$3" fi - local opts="-m -d -l -h -V --modules --disable --no-color --logo --no-logo --list-modules --disk-path --all-disks --json --timings --no-plugins --doctor --help --version" + local opts="-m -d -l -h -V --modules --disable --no-color --logo --logo-variant --compact-logo --compact --no-logo --list-modules --disk-path --all-disks --json --timings --no-plugins --doctor --help --version" local modules="title os host kernel installed uptime packages pkgs shell display desktop de wm terminal term cpu gpu memory mem swap disk battery localip theme icons colors palette" local logos="ferris rust debian ubuntu linuxmint mint fedora arch archlinux rhel redhat centos rocky rockylinux almalinux alma endeavouros endeavour manjaro generic tux linux opensuse suse alpine gentoo void pop popos nixos kali freebsd slackware artix zorin nobara bazzite vylen none" case "${prev}" in + --logo-variant) + COMPREPLY=( $(compgen -W "auto full compact" -- "$cur") ) + return 0 + ;; -m|--modules|-d|--disable) local prefix="" local item="$cur" diff --git a/completions/kkfetch.fish b/completions/kkfetch.fish index b59b10c..4e6783d 100644 --- a/completions/kkfetch.fish +++ b/completions/kkfetch.fish @@ -80,6 +80,9 @@ set -l logos \ complete -c kkfetch -s m -l modules -d 'Enable specific modules in order (comma-separated)' -r -a "$modules" complete -c kkfetch -s d -l disable -d 'Disable specific modules (comma-separated)' -r -a "$modules" complete -c kkfetch -s l -l logo -d 'Override the ASCII logo' -r -a "$logos" +complete -c kkfetch -l logo-variant -d 'Select ASCII logo variant' -r -a 'auto full compact' +complete -c kkfetch -l compact-logo -d 'Force compact ASCII logo layout' +complete -c kkfetch -l compact -d 'Force compact ASCII logo layout (alias)' complete -c kkfetch -l disk-path -d 'Target mount point or directory path for disk usage statistics' -r -F complete -c kkfetch -l all-disks -d 'Show all mounted filesystems and auxiliary partitions' complete -c kkfetch -l no-color -d 'Disable colored ANSI output' diff --git a/src/cli.rs b/src/cli.rs index 3780864..d117cee 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -29,6 +29,14 @@ pub struct Cli { #[arg(short = 'l', long = "logo")] pub logo: Option, + /// Select ASCII logo variant: "auto", "full", or "compact" (default: "auto") + #[arg(long = "logo-variant", value_name = "VARIANT")] + pub logo_variant: Option, + + /// Force compact ASCII logo layout (alias for --logo-variant=compact) + #[arg(long = "compact-logo", alias = "compact")] + pub compact_logo: bool, + /// Do not display any ASCII logo #[arg(long = "no-logo")] pub no_logo: bool, diff --git a/src/config.rs b/src/config.rs index a97d8fa..9b5cf4c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -10,6 +10,7 @@ pub struct Config { pub no_color: Option, pub disk_path: Option, pub all_disks: Option, + pub logo_variant: Option, pub plugins: Option>, } @@ -125,6 +126,12 @@ pub fn parse_config_toml(content: &str) -> Config { "all_disks" | "all-disks" | "disk_all" | "disk-all" => { config.all_disks = Some(val.eq_ignore_ascii_case("true") || val == "1"); } + "logo_variant" | "logo-variant" => { + let clean = val.trim_matches('"').trim_matches('\'').to_string(); + if !clean.is_empty() { + config.logo_variant = Some(clean); + } + } _ => {} } } @@ -209,4 +216,15 @@ command = "git branch --show-current" assert_eq!(plugins[1].name, "Git Branch"); assert_eq!(plugins[1].command, "git branch --show-current"); } + + #[test] + fn test_parse_config_toml_logo_variant() { + let toml_compact = "logo_variant = \"compact\"\n"; + let cfg1 = parse_config_toml(toml_compact); + assert_eq!(cfg1.logo_variant.as_deref(), Some("compact")); + + let toml_full = "logo-variant = 'full'\n"; + let cfg2 = parse_config_toml(toml_full); + assert_eq!(cfg2.logo_variant.as_deref(), Some("full")); + } } diff --git a/src/context.rs b/src/context.rs index 4c4f806..1102f8f 100644 --- a/src/context.rs +++ b/src/context.rs @@ -2,6 +2,7 @@ use crate::cli::Cli; use crate::config::Config; use crate::modules::os::{detect_os, OsInfo}; use crate::modules::ModuleId; +use crate::output::logo::LogoVariant; use std::io::IsTerminal; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -29,6 +30,7 @@ pub struct FetchContext { pub active_modules: Vec, pub logo_override: Option, pub no_logo: bool, + pub logo_variant: LogoVariant, pub no_plugins: bool, pub uname_info: Option, pub theme_info: Option, @@ -59,6 +61,23 @@ impl FetchContext { let logo_override = cli.logo.clone().or_else(|| config.logo.clone()); let no_logo = cli.no_logo || config.no_logo.unwrap_or(false); + let logo_variant = if cli.compact_logo { + LogoVariant::Compact + } else if let Some(ref var_str) = cli.logo_variant { + match var_str.to_lowercase().as_str() { + "compact" => LogoVariant::Compact, + "full" => LogoVariant::Full, + _ => LogoVariant::Auto, + } + } else if let Some(ref cfg_var) = config.logo_variant { + match cfg_var.to_lowercase().as_str() { + "compact" => LogoVariant::Compact, + "full" => LogoVariant::Full, + _ => LogoVariant::Auto, + } + } else { + LogoVariant::Auto + }; let no_plugins = cli.no_plugins; let uname_info = crate::modules::kernel::get_uname_info(); @@ -91,6 +110,7 @@ impl FetchContext { active_modules, logo_override, no_logo, + logo_variant, no_plugins, uname_info, theme_info, @@ -100,14 +120,37 @@ impl FetchContext { } } -/// Detects terminal column width via direct kernel ioctl TIOCGWINSZ, Win32 console info, or $COLUMNS fallback. +/// Detects terminal column width via $COLUMNS override, kernel ioctl TIOCGWINSZ across standard streams and /dev/tty, or Win32 console info. pub fn get_terminal_width() -> u16 { + // Explicit environment variable override takes precedence (e.g. COLUMNS=120 kkfetch) + if let Ok(cols) = std::env::var("COLUMNS") { + if let Ok(c) = cols.trim().parse::() { + if c > 0 { + return c.clamp(20, 1000); + } + } + } + #[cfg(unix)] - // SAFETY: libc::ioctl with TIOCGWINSZ safely writes to a zeroed winsize struct and requires a valid file descriptor (STDOUT_FILENO). + // SAFETY: libc::ioctl with TIOCGWINSZ safely writes to a zeroed winsize struct and requires a valid file descriptor; libc::open and libc::close are invoked with a null-terminated string and valid file descriptor. unsafe { let mut ws: libc::winsize = std::mem::zeroed(); - if libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 { - return ws.ws_col; + for &fd in &[libc::STDOUT_FILENO, libc::STDERR_FILENO, libc::STDIN_FILENO] { + if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 { + return ws.ws_col.clamp(20, 1000); + } + } + + let tty_fd = libc::open( + b"/dev/tty\0".as_ptr() as *const libc::c_char, + libc::O_RDONLY, + ); + if tty_fd >= 0 { + let res = libc::ioctl(tty_fd, libc::TIOCGWINSZ, &mut ws); + libc::close(tty_fd); + if res == 0 && ws.ws_col > 0 { + return ws.ws_col.clamp(20, 1000); + } } } @@ -146,16 +189,7 @@ pub fn get_terminal_width() -> u16 { if GetConsoleScreenBufferInfo(handle, &mut csbi) != 0 { let width = csbi.sr_window.right - csbi.sr_window.left + 1; if width > 0 { - return width as u16; - } - } - } - - // Fallback when stdout is redirected into a pipe or subshell without a tty fd - if let Ok(cols) = std::env::var("COLUMNS") { - if let Ok(c) = cols.trim().parse::() { - if c > 0 { - return c; + return (width as u16).clamp(20, 1000); } } } @@ -205,6 +239,13 @@ pub fn detect_color_level(no_color_flag: bool) -> ColorLevel { if term_lower.contains("256color") { return ColorLevel::Color256; } + if term_lower == "linux" + || term_lower.starts_with("vt100") + || term_lower.starts_with("vt220") + || term_lower == "ansi" + { + return ColorLevel::Basic16; + } } ColorLevel::Basic16 @@ -427,4 +468,43 @@ mod tests { let active = resolve_active_modules(&cli, &config); assert_eq!(active, vec![ModuleId::Os]); } + + #[test] + fn test_fetch_context_logo_variant_resolution() { + // Default is Auto + let cli = Cli::default(); + let config = Config::default(); + let ctx = FetchContext::with_config(&cli, config); + assert_eq!(ctx.logo_variant, LogoVariant::Auto); + + // CLI --compact-logo flag takes priority + let cli_compact_flag = Cli { + compact_logo: true, + ..Default::default() + }; + let ctx_flag = FetchContext::with_config(&cli_compact_flag, Config::default()); + assert_eq!(ctx_flag.logo_variant, LogoVariant::Compact); + + // CLI --logo-variant full + let cli_full = Cli { + logo_variant: Some("full".to_string()), + ..Default::default() + }; + let ctx_full = FetchContext::with_config(&cli_full, Config::default()); + assert_eq!(ctx_full.logo_variant, LogoVariant::Full); + + // Config fallback when CLI is default + let config_compact = Config { + logo_variant: Some("compact".to_string()), + ..Default::default() + }; + let ctx_cfg = FetchContext::with_config(&Cli::default(), config_compact); + assert_eq!(ctx_cfg.logo_variant, LogoVariant::Compact); + } + + #[test] + fn test_get_terminal_width_bounds() { + let width = get_terminal_width(); + assert!((20..=1000).contains(&width)); + } } diff --git a/src/main.rs b/src/main.rs index 6989acd..fb9472d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use clap::Parser; use kkfetch::cli::Cli; use kkfetch::context::FetchContext; use kkfetch::modules::{ModuleId, ModuleRegistry}; -use kkfetch::output::formatter::{render_json, render_layout, render_timings_grid}; +use kkfetch::output::formatter::{render_adaptive_layout, render_json, render_timings_grid}; use kkfetch::output::logo::match_logo; fn main() { @@ -60,8 +60,14 @@ fn main() { ) }; - // Format side-by-side or stacked layout depending on terminal column width - let rendered = render_layout(logo, &outputs, ctx.term_width, ctx.enable_color); + // Format side-by-side or stacked layout adaptively depending on terminal column width, capabilities, and variant + let rendered = render_adaptive_layout( + logo, + &outputs, + ctx.term_width, + ctx.caps.color_level, + Some(ctx.logo_variant), + ); if !rendered.is_empty() { println!("{}", rendered); } diff --git a/src/output/color.rs b/src/output/color.rs index 1bd1233..453405e 100644 --- a/src/output/color.rs +++ b/src/output/color.rs @@ -1,3 +1,5 @@ +use crate::context::ColorLevel; + pub const RESET: &str = "\x1b[0m"; pub const BOLD: &str = "\x1b[1m"; pub const DIM: &str = "\x1b[2m"; @@ -5,6 +7,129 @@ pub const GREEN: &str = "\x1b[32m"; pub const YELLOW: &str = "\x1b[33m"; pub const RED: &str = "\x1b[31m"; +/// Standard 16-color ANSI escape sequences. +pub const ANSI_BLACK: &str = "\x1b[30m"; +pub const ANSI_RED: &str = "\x1b[31m"; +pub const ANSI_GREEN: &str = "\x1b[32m"; +pub const ANSI_YELLOW: &str = "\x1b[33m"; +pub const ANSI_BLUE: &str = "\x1b[34m"; +pub const ANSI_MAGENTA: &str = "\x1b[35m"; +pub const ANSI_CYAN: &str = "\x1b[36m"; +pub const ANSI_WHITE: &str = "\x1b[37m"; + +pub const ANSI_BRIGHT_BLACK: &str = "\x1b[90m"; +pub const ANSI_BRIGHT_RED: &str = "\x1b[91m"; +pub const ANSI_BRIGHT_GREEN: &str = "\x1b[92m"; +pub const ANSI_BRIGHT_YELLOW: &str = "\x1b[93m"; +pub const ANSI_BRIGHT_BLUE: &str = "\x1b[94m"; +pub const ANSI_BRIGHT_MAGENTA: &str = "\x1b[95m"; +pub const ANSI_BRIGHT_CYAN: &str = "\x1b[96m"; +pub const ANSI_BRIGHT_WHITE: &str = "\x1b[97m"; + +/// Coordinates of standard 16 ANSI colors in sRGB space. +const ANSI_16_PALETTE: [(u8, u8, u8, &str); 16] = [ + (0, 0, 0, ANSI_BLACK), + (205, 0, 0, ANSI_RED), + (0, 205, 0, ANSI_GREEN), + (205, 205, 0, ANSI_YELLOW), + (0, 0, 238, ANSI_BLUE), + (205, 0, 205, ANSI_MAGENTA), + (0, 205, 205, ANSI_CYAN), + (229, 229, 229, ANSI_WHITE), + (127, 127, 127, ANSI_BRIGHT_BLACK), + (255, 0, 0, ANSI_BRIGHT_RED), + (0, 255, 0, ANSI_BRIGHT_GREEN), + (255, 255, 0, ANSI_BRIGHT_YELLOW), + (92, 92, 255, ANSI_BRIGHT_BLUE), + (255, 0, 255, ANSI_BRIGHT_MAGENTA), + (0, 255, 255, ANSI_BRIGHT_CYAN), + (255, 255, 255, ANSI_BRIGHT_WHITE), +]; + +const CUBE_STEPS: [u8; 6] = [0, 95, 135, 175, 215, 255]; + +/// Maps a 256-color index `n` (0..=255) to its closest standard 16-color ANSI code. +pub fn color256_to_ansi16(n: u8) -> &'static str { + match n { + 0..=7 => match n { + 0 => ANSI_BLACK, + 1 => ANSI_RED, + 2 => ANSI_GREEN, + 3 => ANSI_YELLOW, + 4 => ANSI_BLUE, + 5 => ANSI_MAGENTA, + 6 => ANSI_CYAN, + _ => ANSI_WHITE, + }, + 8..=15 => match n { + 8 => ANSI_BRIGHT_BLACK, + 9 => ANSI_BRIGHT_RED, + 10 => ANSI_BRIGHT_GREEN, + 11 => ANSI_BRIGHT_YELLOW, + 12 => ANSI_BRIGHT_BLUE, + 13 => ANSI_BRIGHT_MAGENTA, + 14 => ANSI_BRIGHT_CYAN, + _ => ANSI_BRIGHT_WHITE, + }, + 16..=231 => { + let idx = n - 16; + let r_idx = (idx / 36) as usize; + let g_idx = ((idx % 36) / 6) as usize; + let b_idx = (idx % 6) as usize; + + let r = CUBE_STEPS[r_idx]; + let g = CUBE_STEPS[g_idx]; + let b = CUBE_STEPS[b_idx]; + + find_closest_ansi16(r, g, b) + } + 232..=255 => { + let gray = 8 + (n - 232) * 10; + find_closest_ansi16(gray, gray, gray) + } + } +} + +/// Finds the nearest standard 16 ANSI color using squared Euclidean distance in RGB space. +pub fn find_closest_ansi16(r: u8, g: u8, b: u8) -> &'static str { + let mut min_dist = u64::MAX; + let mut closest = ANSI_WHITE; + + for &(ar, ag, ab, seq) in &ANSI_16_PALETTE { + let dr = (r as i64) - (ar as i64); + let dg = (g as i64) - (ag as i64); + let db = (b as i64) - (ab as i64); + let dist = (dr * dr + dg * dg + db * db) as u64; + + if dist < min_dist { + min_dist = dist; + closest = seq; + } + } + + closest +} + +/// Resolves an ANSI color string according to the target terminal color capability. +/// Degrades 256-color sequences to 16-color ANSI on `Basic16` and strips them completely on `None`. +pub fn resolve_color(code: &'static str, level: ColorLevel) -> &'static str { + match level { + ColorLevel::None => "", + ColorLevel::Basic16 => { + if let Some(inner) = code + .strip_prefix("\x1b[38;5;") + .and_then(|s| s.strip_suffix('m')) + { + if let Ok(n) = inner.parse::() { + return color256_to_ansi16(n); + } + } + code + } + ColorLevel::Color256 | ColorLevel::TrueColor => code, + } +} + /// Applies an ANSI escape code to a string if color is enabled. pub fn style(text: &str, ansi_code: &str, enabled: bool) -> String { if enabled && !ansi_code.is_empty() { @@ -135,4 +260,43 @@ mod tests { assert_eq!(format_percentage(35, true, false), "35%"); assert_eq!(format_percentage(10, true, false), "10%"); } + + #[test] + fn test_color256_to_ansi16_standard_and_bright() { + assert_eq!(color256_to_ansi16(1), ANSI_RED); + assert_eq!(color256_to_ansi16(2), ANSI_GREEN); + assert_eq!(color256_to_ansi16(4), ANSI_BLUE); + assert_eq!(color256_to_ansi16(9), ANSI_BRIGHT_RED); + assert_eq!(color256_to_ansi16(10), ANSI_BRIGHT_GREEN); + assert_eq!(color256_to_ansi16(14), ANSI_BRIGHT_CYAN); + } + + #[test] + fn test_color256_to_ansi16_cube_fallbacks() { + // Red 196 + assert_eq!(color256_to_ansi16(196), ANSI_BRIGHT_RED); + // Orange 208 maps to ANSI yellow/orange + assert_eq!(color256_to_ansi16(208), ANSI_YELLOW); + // White 231 maps to bright white + assert_eq!(color256_to_ansi16(231), ANSI_BRIGHT_WHITE); + // Lime green 46 maps to bright green + assert_eq!(color256_to_ansi16(46), ANSI_BRIGHT_GREEN); + // Sky blue 39 maps to cyan (closest distance in sRGB) + assert_eq!(color256_to_ansi16(39), ANSI_CYAN); + } + + #[test] + fn test_resolve_color_levels() { + let orange_256 = "\x1b[38;5;208m"; + + // None strips color + assert_eq!(resolve_color(orange_256, ColorLevel::None), ""); + + // Basic16 degrades 256-color code to 16-color ANSI sequence + assert_eq!(resolve_color(orange_256, ColorLevel::Basic16), ANSI_YELLOW); + + // Color256 and TrueColor preserve full fidelity + assert_eq!(resolve_color(orange_256, ColorLevel::Color256), orange_256); + assert_eq!(resolve_color(orange_256, ColorLevel::TrueColor), orange_256); + } } diff --git a/src/output/formatter.rs b/src/output/formatter.rs index 971f536..06fbdaa 100644 --- a/src/output/formatter.rs +++ b/src/output/formatter.rs @@ -1,6 +1,7 @@ +use crate::context::ColorLevel; use crate::modules::ModuleOutput; -use crate::output::color::format_label; -use crate::output::logo::Logo; +use crate::output::color::{format_label, resolve_color}; +use crate::output::logo::{Logo, LogoVariant}; /// Returns the column display width of a Unicode character (1 for standard, 2 for wide CJK/emojis). /// Adheres to Unicode Standard Annex #11 (East Asian Width) for terminal cell occupancy. @@ -40,14 +41,33 @@ pub fn visible_width(s: &str) -> usize { len } -/// Renders the complete fetch layout with logo and module info lines. +/// Renders the complete fetch layout with logo and module info lines (backwards-compatible). pub fn render_layout( logo: Option<&Logo>, outputs: &[ModuleOutput], term_width: u16, enable_color: bool, ) -> String { - let distro_color = logo.map(|l| l.distro_color).unwrap_or("\x1b[38;5;208m"); + let color_level = if enable_color { + ColorLevel::Color256 + } else { + ColorLevel::None + }; + render_adaptive_layout(logo, outputs, term_width, color_level, None) +} + +/// Renders the complete fetch layout adaptively with logo and module info lines based on +/// terminal dimensions and color capability. +pub fn render_adaptive_layout( + logo: Option<&Logo>, + outputs: &[ModuleOutput], + term_width: u16, + color_level: ColorLevel, + forced_variant: Option, +) -> String { + let raw_color = logo.map(|l| l.distro_color).unwrap_or("\x1b[38;5;208m"); + let distro_color = resolve_color(raw_color, color_level); + let enable_color = color_level != ColorLevel::None; // Flatten module outputs into display lines let mut info_lines: Vec = Vec::new(); @@ -69,25 +89,102 @@ pub fn render_layout( None => return info_lines.join("\n"), }; - let logo_lines = logo.render_lines(enable_color); - - // Narrow terminal breakpoint: switch to vertical stacked layout below 60 columns to prevent line-wrapping - if term_width < 60 { - let mut full_output = Vec::new(); - full_output.extend(logo_lines); - if !info_lines.is_empty() { - full_output.push(String::new()); - full_output.extend(info_lines); - } - return full_output.join("\n"); - } + let full_lines = logo.render_variant_lines(LogoVariant::Full, color_level); + let compact_lines = logo.render_variant_lines(LogoVariant::Compact, color_level); - // Side-by-side two-column layout: calculate max logo width to align the info column - let max_logo_width = logo_lines + let full_width = full_lines + .iter() + .map(|l| visible_width(l)) + .max() + .unwrap_or(0); + let compact_width = compact_lines .iter() .map(|l| visible_width(l)) .max() .unwrap_or(0); + let max_info_width = info_lines + .iter() + .map(|l| visible_width(l)) + .max() + .unwrap_or(0); + + let term_w = term_width as usize; + let separator_w = 3; + + // Constraint solver: determine variant and layout geometry + enum SelectedLayout { + SideBySide(LogoVariant, usize), + Stacked(LogoVariant), + InfoOnly, + } + + let selected = match forced_variant.unwrap_or(LogoVariant::Auto) { + LogoVariant::Full => { + if term_w >= 60 && full_width + separator_w + max_info_width <= term_w { + SelectedLayout::SideBySide(LogoVariant::Full, full_width) + } else { + SelectedLayout::Stacked(LogoVariant::Full) + } + } + LogoVariant::Compact => { + if term_w >= 50 && compact_width + separator_w + max_info_width <= term_w { + SelectedLayout::SideBySide(LogoVariant::Compact, compact_width) + } else { + SelectedLayout::Stacked(LogoVariant::Compact) + } + } + LogoVariant::Auto => { + if term_w < 60 { + // Stacked below 60 columns to prevent line cramping + if full_width <= term_w && term_w >= 45 { + SelectedLayout::Stacked(LogoVariant::Full) + } else if compact_width <= term_w && term_w >= 20 { + SelectedLayout::Stacked(LogoVariant::Compact) + } else { + SelectedLayout::InfoOnly + } + } else { + // Width >= 60: attempt side-by-side + if full_width + separator_w + max_info_width <= term_w { + SelectedLayout::SideBySide(LogoVariant::Full, full_width) + } else if compact_width + separator_w + max_info_width <= term_w { + SelectedLayout::SideBySide(LogoVariant::Compact, compact_width) + } else if full_width <= term_w { + SelectedLayout::Stacked(LogoVariant::Full) + } else if compact_width <= term_w { + SelectedLayout::Stacked(LogoVariant::Compact) + } else { + SelectedLayout::InfoOnly + } + } + } + }; + + let (logo_lines, max_logo_width) = match selected { + SelectedLayout::SideBySide(variant, width) => { + let lines = match variant { + LogoVariant::Compact => compact_lines, + _ => full_lines, + }; + (lines, width) + } + SelectedLayout::Stacked(variant) => { + let lines = match variant { + LogoVariant::Compact => compact_lines, + _ => full_lines, + }; + let mut full_output = Vec::new(); + full_output.extend(lines); + if !info_lines.is_empty() { + full_output.push(String::new()); + full_output.extend(info_lines); + } + return full_output.join("\n"); + } + SelectedLayout::InfoOnly => { + return info_lines.join("\n"); + } + }; let row_count = std::cmp::max(logo_lines.len(), info_lines.len()); let mut rows: Vec = Vec::with_capacity(row_count); @@ -338,6 +435,7 @@ mod tests { let logo = Logo { name: "test", raw_lines: &["AA", "BB"], + compact_lines: None, distro_color: "", outer_color: "", }; @@ -358,6 +456,7 @@ mod tests { let logo = Logo { name: "test", raw_lines: &["A", "BBB"], + compact_lines: None, distro_color: "", outer_color: "", }; @@ -384,6 +483,69 @@ mod tests { assert_eq!(lines[1], "BBB Kernel: 6.1"); } + #[test] + fn test_render_adaptive_layout_compact_breakpoint() { + let logo = Logo { + name: "test", + raw_lines: &[ + "1234567890123456789012345678901234567890", // 40 chars wide + ], + compact_lines: Some(&[ + "1234567890", // 10 chars wide + ]), + distro_color: "", + outer_color: "", + }; + + let outputs = vec![ModuleOutput { + id: ModuleId::Os, + label: "OS".to_string(), + value: "Linux with long info name".to_string(), + custom_rendered: None, + }]; + // max_info_width = ~30. 40 + 3 + 30 = 73 cols. + // On term_width = 65, full logo (40 + 3 + 30 = 73) does not fit side-by-side. + // But compact logo (10 + 3 + 30 = 43 <= 65) DOES fit side-by-side! + let rendered = render_adaptive_layout( + Some(&logo), + &outputs, + 65, + ColorLevel::None, + Some(LogoVariant::Auto), + ); + // Verify it rendered side-by-side using the compact logo! + assert!(rendered.contains("1234567890 OS: Linux with long info name")); + } + + #[test] + fn test_render_adaptive_layout_forced_compact() { + let logo = Logo { + name: "test", + raw_lines: &["FULL_LOGO_LINE"], + compact_lines: Some(&["COMPACT_LINE"]), + distro_color: "", + outer_color: "", + }; + + let outputs = vec![ModuleOutput { + id: ModuleId::Os, + label: "OS".to_string(), + value: "Linux".to_string(), + custom_rendered: None, + }]; + + // Even with 120 cols, forcing compact outputs compact variant + let rendered = render_adaptive_layout( + Some(&logo), + &outputs, + 120, + ColorLevel::None, + Some(LogoVariant::Compact), + ); + assert!(rendered.contains("COMPACT_LINE")); + assert!(!rendered.contains("FULL_LOGO_LINE")); + } + #[test] fn test_visible_width_wide_cjk() { // CJK characters take 2 columns each diff --git a/src/output/logo.rs b/src/output/logo.rs index 08183ed..82cd73c 100644 --- a/src/output/logo.rs +++ b/src/output/logo.rs @@ -5,7 +5,16 @@ //! KKFetch with high-contrast white structural framing (`{w}`) and //! distribution brand signature color emblems (`{c}`). -use crate::output::color::RESET; +use crate::context::ColorLevel; +use crate::output::color::{resolve_color, RESET}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum LogoVariant { + #[default] + Auto, + Full, + Compact, +} pub const WHITE_COLOR: &str = "\x1b[38;5;231m"; @@ -23,27 +32,64 @@ pub const RAINBOW_PALETTE: [(&str, &str); 6] = [ pub struct Logo { pub name: &'static str, pub raw_lines: &'static [&'static str], + pub compact_lines: Option<&'static [&'static str]>, pub distro_color: &'static str, pub outer_color: &'static str, } impl Logo { - /// Returns the logo lines formatted with ANSI colors if enabled. + /// Returns the compact ASCII lines if defined, or falls back to generic compact Tux penguin or full art. + pub fn compact_lines_or_fallback(&self) -> &'static [&'static str] { + if let Some(lines) = self.compact_lines { + lines + } else { + ALL_LOGOS + .iter() + .find(|l| l.name == "generic") + .and_then(|g| g.compact_lines) + .unwrap_or(self.raw_lines) + } + } + + /// Returns the logo lines formatted with ANSI colors if enabled (backwards-compatible). /// The outer layer is styled with `outer_color` (white), and the /// highlighted inner emblem is styled with `distro_color`. /// Numbered palette tokens `{1}`..`{6}` provide multi-color stripe /// support for vintage logos such as the 1977 Apple rainbow. pub fn render_lines(&self, enable_color: bool) -> Vec { - self.raw_lines + let level = if enable_color { + ColorLevel::Color256 + } else { + ColorLevel::None + }; + self.render_variant_lines(LogoVariant::Full, level) + } + + /// Returns rendered lines for a specific logo variant and color level. + pub fn render_variant_lines( + &self, + variant: LogoVariant, + color_level: ColorLevel, + ) -> Vec { + let lines = match variant { + LogoVariant::Compact => self.compact_lines_or_fallback(), + _ => self.raw_lines, + }; + + let enable_color = color_level != ColorLevel::None; + let d_color = resolve_color(self.distro_color, color_level); + let o_color = resolve_color(self.outer_color, color_level); + + lines .iter() .map(|line| { if enable_color { let mut rendered = line - .replace("{c}", self.distro_color) - .replace("{w}", self.outer_color) + .replace("{c}", d_color) + .replace("{w}", o_color) .replace("{0}", RESET); for &(token, color) in &RAINBOW_PALETTE { - rendered = rendered.replace(token, color); + rendered = rendered.replace(token, resolve_color(color, color_level)); } let has_color = line.contains("{c}") @@ -53,7 +99,7 @@ impl Logo { .any(|(token, _)| line.contains(token)); if !has_color && !line.is_empty() { - rendered = format!("{}{}{}", self.distro_color, line, RESET); + rendered = format!("{}{}{}", d_color, line, RESET); } else if !line.is_empty() { rendered.push_str(RESET); } @@ -88,6 +134,12 @@ pub const ALL_LOGOS: &[Logo] = &[ " {c}'-._'-------'_.-'", " {w}/ \\", ], + compact_lines: Some(&[ + " {w}_~^~^~_", + " {w}\\) {c}/ o o \\ {w}\\(", + " {c}'_ - _'", + " {c}/ '-----' \\", + ]), distro_color: "\x1b[38;5;208m", outer_color: WHITE_COLOR, }, @@ -112,6 +164,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} `\"Y$b._", "{c} `\"\"\"", ], + compact_lines: Some(&[ + "{c} _____", + "{c} / __ \\", + "{c}| / |", + "{c}| \\___-", + "{c} -_", + "{c} --_", + ]), distro_color: "\x1b[38;5;196m", outer_color: WHITE_COLOR, }, @@ -139,6 +199,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} `:+ssssssssssssssssss+:`", "{w} .-\\+oossssoo+/-.", ], + compact_lines: Some(&[ + "{c} _", + "{w} ---{c}(_)", + "{w} _/{c} --- {w}\\", + "{c} (_){w} | |", + "{w} \\ {c} --- {w}_/", + "{w} ---{c}(_)", + ]), distro_color: "\x1b[38;5;208m", outer_color: WHITE_COLOR, }, @@ -165,6 +233,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} '-MMMMMMMMMMMMM-'", "{w} ``-:::::-``", ], + compact_lines: Some(&[ + "{w} _________", + "{w}|_ \\", + "{w} | | {c}_____{w} |", + "{w} | | {c}| | |{w} |", + "{w} | | {c}|_|_|{w} |", + "{w} | \\_____/|", + "{w} \\________/", + ]), distro_color: "\x1b[38;5;46m", outer_color: WHITE_COLOR, }, @@ -191,6 +268,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w}.:cccccccccccccccccccccc:;,..", "{w} '::cccccccccccccc::;,.", ], + compact_lines: Some(&[ + "{c} ____", + "{c} / /", + "{w} ___/{c} / /", + "{w} / _ {c}/ /", + "{w} / /_/ {c}/ /", + "{w}/_____{c}/_/", + ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, }, @@ -217,6 +302,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} `++:. `-/+/", "{w} .` `/", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} / \\", + "{c} /\\ \\", + "{c} / \\", + "{c} / ,, \\", + "{c} / | | -\\", + "{c}/_-'' ''-_/", + ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, }, @@ -240,6 +334,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} MMMMMMMMMMMMMMMMyoNNNMMM+. {w}MMMMMMMM", "{c} MMNMMMNNMMMMMNM+ mhsMNyyyyMNMMMMsMM", ], + compact_lines: Some(&[ + "{c} .---.", + "{c} / \\", + "{c} | {w}() (){c} |", + "{c} \\ - /", + "{c} '---'", + ]), distro_color: "\x1b[38;5;196m", outer_color: WHITE_COLOR, }, @@ -259,6 +360,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} `-` ./+++++++++++-", "{c} -+++++++++:-.`", ], + compact_lines: Some(&[ + "{c} .---.", + "{c} / / \\", + "{c}| / |", + "{c} \\ / /", + "{c} '---'", + ]), distro_color: "\x1b[38;5;35m", outer_color: WHITE_COLOR, }, @@ -286,6 +394,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ;kkkkl", "{w} ,od;", ], + compact_lines: Some(&[ + "{c} .---.", + "{c} / | \\", + "{c} | --+-- |", + "{c} \\ | /", + "{c} '---'", + ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, }, @@ -308,6 +423,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ./++++++++++++++++++++++++++++++/:.", "{w} `:::::::::::::::::::::::::------``", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} // \\\\", + "{w} // \\\\", + "{c} // \\\\", + "{w}/__________\\\\", + ]), distro_color: "\x1b[38;5;127m", outer_color: WHITE_COLOR, }, @@ -329,6 +451,12 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c}████████ {w}████████ {w}████████", "{c}████████ {w}████████ {w}████████", ], + compact_lines: Some(&[ + "{c}|||| |||| ||||", + "{c}|||| ", + "{c}|||| |||| ||||", + "{c}|||| |||| ||||", + ]), distro_color: "\x1b[38;5;34m", outer_color: WHITE_COLOR, }, @@ -352,6 +480,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} ####{w}#####{c}####", "{c} ### ###", ], + compact_lines: Some(&[ + "{w} .--.", + "{w} |{c}o_o {w}|", + "{w} |{c}:_/ {w}|", + "{w} // \\ \\", + "{w}(| | )", + "{c}/'\\_ _/`\\", + "{c}\\___)=(___/", + ]), distro_color: "\x1b[38;5;220m", outer_color: WHITE_COLOR, }, @@ -375,6 +512,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} .;d00xl:^''''''^:ok00d;.", "{w} .;ldkO0000Okdl;.", ], + compact_lines: Some(&[ + "{c} _______", + "{c} __| __ \\", + "{c}/ *\\ / \\", + "{c}\\__ _/", + "{c} \\_____/", + ]), distro_color: "\x1b[38;5;71m", outer_color: WHITE_COLOR, }, @@ -396,6 +540,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} :dddddddddddddddddddddddddd:", "{w} .hddddddddddddddddddddddh.", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} // \\", + "{c} // \\", + "{c}/// \\", + "{c}//' \\", + ]), distro_color: "\x1b[38;5;32m", outer_color: WHITE_COLOR, }, @@ -416,6 +567,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} `:+syd{w}hyyyyyhddddds{w}+", "{w} `-:+syhhhysoo+.", ], + compact_lines: Some(&[ + "{c} _-----_", + "{c}( \\", + "{c}\\ {w}0{c} \\", + "{c} \\ )", + "{c} / /", + "{c}(_____-'", + ]), distro_color: "\x1b[38;5;141m", outer_color: WHITE_COLOR, }, @@ -442,6 +601,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} _.=+==++=++=+=+===;.", "{w} __.;=====;.__", ], + compact_lines: Some(&[ + "{c} _______", + "{c} \\ ____)", + "{c} \\ \\", + "{c} \\ \\ __", + "{c} \\ \\ ( \\", + "{c} \\ \\_\\ \\", + "{c} \\______)", + ]), distro_color: "\x1b[38;5;35m", outer_color: WHITE_COLOR, }, @@ -466,6 +634,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} -+ydmNNNNNNNmdy+-", "{w} `.-:::-.`", ], + compact_lines: Some(&[ + "{c}______", + "{c}\\ _ \\", + "{c} \\ \\_) )", + "{c} \\ __/", + "{c} \\_\\", + ]), distro_color: "\x1b[38;5;37m", outer_color: WHITE_COLOR, }, @@ -499,6 +674,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ▟███▛ ▜███▙ {c}▜███▙", "{w} ▝▀▀▀ ▀▀▀▀▘ {c}▀▀▀▘", ], + compact_lines: Some(&[ + "{c} \\\\ //", + "{c} \\\\//", + "{w} == > < ==", + "{c} //\\\\", + "{c} // \\\\", + ]), distro_color: "\x1b[38;5;75m", outer_color: WHITE_COLOR, }, @@ -527,6 +709,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} {c}.'{w}", "{w} {c}.{w}", ], + compact_lines: Some(&[ + "{c} ..............", + "{c} ...,", + "{c} .---'", + "{c} / ", + "{c} / ", + ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, }, @@ -549,6 +738,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} .-- `--.", "{w} .---.....----.", ], + compact_lines: Some(&[ + "{c}/\\,-'''''-,/\\", + "{c}\\_) (_/", + "{c}| |", + "{c}| |", + "{c} ; ;", + "{c} '-_____-'", + ]), distro_color: "\x1b[38;5;196m", outer_color: WHITE_COLOR, }, @@ -577,6 +774,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} :::::::::::::::::::::: ", "{w} :::::::::::: ", ], + compact_lines: Some(&[ + "{c} ________", + "{c} / _____/", + "{c} \\____ \\", + "{c} / \\", + "{c} /______ /", + "{c} \\/", + ]), distro_color: "\x1b[38;5;61m", outer_color: WHITE_COLOR, }, @@ -604,6 +809,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} 'i:'` ''{c}:io'", "{w}'` `'", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} / \\", + "{c} /`'.,\\", + "{c} / ',", + "{c} / ,/", + "{c} /__,.-'`", + ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, }, @@ -628,6 +841,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} -osssssssssssssssssssssso-", "{w} `osssssssssssssssssssso`", ], + compact_lines: Some(&[ + "{c} ______", + "{c}|___ /", + "{c} / / ", + "{c} / /__", + "{c} /_____|", + ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, }, @@ -654,6 +874,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ````''*::cll", "{w} ``", ], + compact_lines: Some(&[ + "{c}#### ####", + "{c}#### ####", + "", + "{c}#### ####", + "{c}#### ####", + ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, }, @@ -680,6 +907,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ````''*::cll", "{w} ``", ], + compact_lines: Some(&[ + "{c} ,. .....", + "{c},$$$ $$$$$", + "{c}$$$$ $$$$$", + "{c} ", + "{c}$$$$ $$$$$", + "{c}`$$$ $$$$$", + ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, }, @@ -703,6 +938,12 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} ` :EEEEtttt::::z7", "{c} \"VEzjt:;;z>*`", ], + compact_lines: Some(&[ + "{1} ,. {2}.....", + "{1}$$$ {2}$$$$$", + "{4}$$$ {3}$$$$$", + "{4} `' {3}`````", + ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, }, @@ -729,6 +970,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} MMMMo oMMMM", "{c} ooss ssoo", ], + compact_lines: Some(&[ + "{c} __ __", + "{c} | \\_/ |", + "{c} | {w}O O{c} |", + "{c} \\_______/", + "{c} | |", + ]), distro_color: "\x1b[38;5;118m", outer_color: WHITE_COLOR, }, @@ -753,6 +1001,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{6} ;KMMMMMMMWXXWMMMMMMMk.", "{6} \"cooc*\" \"*coo'\"", ], + compact_lines: Some(&[ + "{1} .:'", + "{2} __ :'__", + "{3}.' `-' `.", + "{4}: :", + "{5}: :", + "{6} : :", + "{6} `-.__.-'", + ]), distro_color: "\x1b[38;5;250m", outer_color: WHITE_COLOR, }, @@ -773,6 +1030,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} \\__ \\._/ \\_/__/ ./", "{c} `---._____.-'", ], + compact_lines: Some(&[ + "{w} _.", + "{c} .-'{w}*{c}`-.", + "{c} .' {w}O O{c} `.", + "{c} : {w}(____){c} :", + "{c} `. .'", + "{c} `-...-'", + ]), distro_color: "\x1b[38;5;220m", outer_color: WHITE_COLOR, }, @@ -793,6 +1058,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} {w}/ / \\\\ \\\\{c} \\\\", "{c} {w}/__/{c} {w}\\\\__\\\\{c} \\\\", ], + compact_lines: Some(&[ + "{w}\\\\`-____", + "{c} \\\\", + "{c} \\\\", + "{c} \\\\", + "{c} \\\\", + ]), distro_color: "\x1b[38;5;208m", outer_color: WHITE_COLOR, }, @@ -822,6 +1094,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} .-=======================+++:", "{w} ..........................", ], + compact_lines: Some(&[ + "{c} ____", + "{c} .' `.", + "{c} / __ \\", + "{c} | / \\ |", + "{c} \\ \\__/ /", + "{c} `.____.'", + ]), distro_color: "\x1b[38;5;43m", outer_color: WHITE_COLOR, }, @@ -844,6 +1124,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c}██ {w}██ {c}██", "{c}██████████████████ █████████████", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} / \\", + "{c} / () \\", + "{c} / \\", + "{c} /________\\", + ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, }, @@ -869,6 +1156,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} d:SS@8ba89aa67a853Sxxad.", "{w} .d988999889889899dd.", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} / \\", + "{c} / /\\ \\", + "{c} / / \\ \\", + "{c} /_/ \\_\\", + ]), distro_color: "\x1b[38;5;208m", outer_color: WHITE_COLOR, }, @@ -897,6 +1191,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} `++:. {c}//{w} `-/+/", "{w} .` {c}/{w} `/", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} / \\", + "{c} / || \\", + "{c} / || \\", + "{c} /___||___\\", + ]), distro_color: "\x1b[38;5;196m", outer_color: WHITE_COLOR, }, @@ -928,6 +1229,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} -+", "{w} -", ], + compact_lines: Some(&[ + "{c} .---.", + "{c} / O \\", + "{c} | /| |", + "{c} \\ \\| /", + "{c} '---'", + ]), distro_color: "\x1b[38;5;44m", outer_color: WHITE_COLOR, }, @@ -955,6 +1263,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ./oyyyyy{c}yyyyyyyyyyyyyo{w}/.", "{w} `.:/oosyyyysso/:.`", ], + compact_lines: Some(&[ + "{c} ___", + "{c} / \\", + "{c} | O |", + "{c} \\ /", + "{c} ---", + ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, }, @@ -979,6 +1294,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} ⠘⢿⣿⣿⣿⣿⣿⣿⣿⡿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠈⢿⠀⠀⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣿⣿⣿⠟⠁", "{c} ⠈⠙⠛⠛⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠛⠛⠉⠁", ], + compact_lines: Some(&[ + "{c} /\\", + "{c} / \\", + "{c} / /\\ \\", + "{c} / / \\ \\", + "{c} /_/ \\_\\", + ]), distro_color: "\x1b[38;5;197m", outer_color: WHITE_COLOR, }, @@ -1005,6 +1327,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} ###################", "{c} ###########", ], + compact_lines: Some(&[ + "{c} .---.", + "{c} / B \\", + "{c} | B |", + "{c} \\ B /", + "{c} '---'", + ]), distro_color: "\x1b[38;5;141m", outer_color: WHITE_COLOR, }, @@ -1031,6 +1360,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} 'MKKM'", "{w} ''", ], + compact_lines: Some(&[ + "{c} .---.", + "{c} / | \\", + "{c} | --+-- |", + "{c} \\ | /", + "{c} '---'", + ]), distro_color: "\x1b[38;5;214m", outer_color: WHITE_COLOR, }, @@ -1057,6 +1393,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} .:^^^^^^^^^^^^^^^^^^^^:.", "{c} .^^^^^^^^^^^^^^^^^^.", ], + compact_lines: Some(&[ + "{c} \\ /", + "{c} \\ / ", + "{c} \\ / ", + "{c} \\ / ", + "{c} v ", + ]), distro_color: "\x1b[38;5;2m", outer_color: WHITE_COLOR, }, @@ -1374,4 +1717,57 @@ pub mod tests { ); assert_eq!(match_logo(None, "vylen", &[]).unwrap().name, "vylen"); } + + #[test] + fn test_compact_logo_variants_defined_and_renderable() { + for logo in ALL_LOGOS { + let compact = logo.compact_lines_or_fallback(); + assert!( + !compact.is_empty(), + "Compact lines for {} must not be empty", + logo.name + ); + + // Render both plain and colored variant lines + let plain = logo.render_variant_lines(LogoVariant::Compact, ColorLevel::None); + assert_eq!(plain.len(), compact.len()); + assert!( + !plain.iter().any(|l| l.contains("\x1b")), + "Monochrome compact lines must strip escapes" + ); + + let colored = logo.render_variant_lines(LogoVariant::Compact, ColorLevel::Color256); + assert_eq!(colored.len(), compact.len()); + } + } + + #[test] + fn test_compact_logo_dimensions_smaller_than_full() { + // Arch + let arch = match_logo(Some("arch"), "arch", &[]).unwrap(); + assert!(arch.compact_lines_or_fallback().len() < arch.raw_lines.len()); + + // Debian + let debian = match_logo(Some("debian"), "debian", &[]).unwrap(); + assert!(debian.compact_lines_or_fallback().len() < debian.raw_lines.len()); + + // Ubuntu + let ubuntu = match_logo(Some("ubuntu"), "ubuntu", &[]).unwrap(); + assert!(ubuntu.compact_lines_or_fallback().len() < ubuntu.raw_lines.len()); + + // Fedora + let fedora = match_logo(Some("fedora"), "fedora", &[]).unwrap(); + assert!(fedora.compact_lines_or_fallback().len() < fedora.raw_lines.len()); + } + + #[test] + fn test_render_variant_lines_color_level_basic16() { + let debian = match_logo(Some("debian"), "debian", &[]).unwrap(); + let rendered_basic16 = debian.render_variant_lines(LogoVariant::Full, ColorLevel::Basic16); + // Debian distro_color is 196 (red). On basic16, it degrades to \x1b[91m or \x1b[31m, never \x1b[38;5; + assert!(!rendered_basic16.iter().any(|l| l.contains("\x1b[38;5;"))); + assert!(rendered_basic16 + .iter() + .any(|l| l.contains("\x1b[91m") || l.contains("\x1b[31m"))); + } } diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index cbe20ec..8e3d0d2 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -74,6 +74,54 @@ fn test_logo_override_flag() { cmd.args(["--no-color", "--logo", "arch", "-m", "os"]); let assert = cmd.assert().success(); let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + assert!(stdout.contains("OS:")); + assert!(stdout.contains("/\\")); +} + +#[test] +fn test_logo_variant_compact_flag() { + let mut cmd = Command::cargo_bin("kkfetch").unwrap(); + cmd.args(["--no-color", "--logo", "arch", "--compact-logo", "-m", "os"]); + let assert = cmd.assert().success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + assert!(stdout.contains("/_-''")); + assert!(!stdout.contains("oooo")); + assert!(stdout.contains("OS:")); +} + +#[test] +fn test_logo_variant_option_compact() { + let mut cmd = Command::cargo_bin("kkfetch").unwrap(); + cmd.args([ + "--no-color", + "--logo", + "arch", + "--logo-variant", + "compact", + "-m", + "os", + ]); + let assert = cmd.assert().success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); + assert!(stdout.contains("/_-''")); + assert!(!stdout.contains("oooo")); + assert!(stdout.contains("OS:")); +} + +#[test] +fn test_logo_variant_option_full() { + let mut cmd = Command::cargo_bin("kkfetch").unwrap(); + cmd.args([ + "--no-color", + "--logo", + "arch", + "--logo-variant", + "full", + "-m", + "os", + ]); + let assert = cmd.assert().success(); + let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); assert!(stdout.contains("oooo")); assert!(stdout.contains("OS:")); } @@ -193,7 +241,15 @@ fn test_various_logo_overrides() { for (name, snippet) in logos { let mut cmd = Command::cargo_bin("kkfetch").unwrap(); - cmd.args(["--no-color", "--logo", name, "-m", "os"]); + cmd.args([ + "--no-color", + "--logo", + name, + "--logo-variant", + "full", + "-m", + "os", + ]); let assert = cmd.assert().success(); let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); assert!( diff --git a/tests/completion_tests.rs b/tests/completion_tests.rs index d590320..5f660ef 100644 --- a/tests/completion_tests.rs +++ b/tests/completion_tests.rs @@ -33,6 +33,8 @@ fn test_completion_files_exist_and_cover_all_flags() { "--modules", "--disable", "--logo", + "--logo-variant", + "--compact-logo", "--no-color", "--no-logo", "--list-modules", From 1e82fb1012cd9c891091e8a1dbe2ec38b095a516 Mon Sep 17 00:00:00 2001 From: Kushagra Kumar Date: Sat, 7 Nov 2026 03:55:53 +0530 Subject: [PATCH 2/4] test(cli): support adaptive logo variant matching in cli tests --- tests/cli_tests.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/cli_tests.rs b/tests/cli_tests.rs index 8e3d0d2..5ac7530 100644 --- a/tests/cli_tests.rs +++ b/tests/cli_tests.rs @@ -75,7 +75,8 @@ fn test_logo_override_flag() { let assert = cmd.assert().success(); let stdout = String::from_utf8(assert.get_output().stdout.clone()).unwrap(); assert!(stdout.contains("OS:")); - assert!(stdout.contains("/\\")); + // Arch logo is matched and rendered adaptively (full or compact variant) + assert!(stdout.contains("oooo") || stdout.contains("/_-''")); } #[test] From 22d054332d00067d33ff668ebd19d909239231e0 Mon Sep 17 00:00:00 2001 From: Kushagra Kumar Date: Sat, 7 Nov 2026 04:44:04 +0530 Subject: [PATCH 3/4] style(logo): polish compact distro ascii art inspired by fastfetch design language --- src/output/logo.rs | 454 ++++++++++++++++++++++++++------------------- 1 file changed, 268 insertions(+), 186 deletions(-) diff --git a/src/output/logo.rs b/src/output/logo.rs index 82cd73c..01fb397 100644 --- a/src/output/logo.rs +++ b/src/output/logo.rs @@ -269,12 +269,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} '::cccccccccccccc::;,.", ], compact_lines: Some(&[ - "{c} ____", - "{c} / /", - "{w} ___/{c} / /", - "{w} / _ {c}/ /", - "{w} / /_/ {c}/ /", - "{w}/_____{c}/_/", + "{c} ,'''''.", + "{c} | {w},. {c}|", + "{c} | {w}| '_'{c}", + "{w} ,....{c}| {w}|..", + "{w}.' ,_;{c}| {w}..'", + "{w}| | {c}| {w}|", + "{w}| ',_,' {w}|", + "{w} '. ,'", + "{w} '''''", ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, @@ -335,11 +338,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} MMNMMMNNMMMMMNM+ mhsMNyyyyMNMMMMsMM", ], compact_lines: Some(&[ - "{c} .---.", - "{c} / \\", - "{c} | {w}() (){c} |", - "{c} \\ - /", - "{c} '---'", + "{c} .M.:MMM", + "{c} MMMMMMMMMM.", + "{c} ,MMMMMMMMMMM", + "{c} .MM MMMMMMMMMMM", + "{c}MMMM MMMMMMMMM", + "{c}MMMMMM MM", + "{c} MMMMMMMMM ,MMMM", + "{c} MMMMMMMMMMMMMMMM:", + "{c} `MMMMMMMMMMMM", ]), distro_color: "\x1b[38;5;196m", outer_color: WHITE_COLOR, @@ -361,11 +368,18 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} -+++++++++:-.`", ], compact_lines: Some(&[ - "{c} .---.", - "{c} / / \\", - "{c}| / |", - "{c} \\ / /", - "{c} '---'", + "{c} `-/+++++++++/-.`", + "{c} `-+++++++++++++++++-`", + "{c}.+++++++++++++++++++++.", + "{c}-+++++++++++++++++++++++.", + "{c}+++++++++++++++/-/+++++++", + "{c}+++++++++++++/. ./+++++", + "{c}+++++++++++:. ./+++", + "{c}+++++++++:` `:/:` .:/", + "{c}-++++++:` .:+++++:`", + "{c} .+++-` ./+++++++++:`", + "{c} `-` ./+++++++++++-", + "{c} -+++++++++:-.`", ]), distro_color: "\x1b[38;5;35m", outer_color: WHITE_COLOR, @@ -395,11 +409,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ,od;", ], compact_lines: Some(&[ - "{c} .---.", - "{c} / | \\", - "{c} | --+-- |", - "{c} \\ | /", - "{c} '---'", + "{4} 'c:. {1}.. ,cc,", + "{4} lkkk; {1}.lxkk.okkd", + "{4} .xkkkd {1};kx. .lkx", + "{6}.'lkkko' {2}ckkkx.", + "{6}'xkkd:kk' {5}ld.{2}:kkxo", + "{6} 'dkkko: {5}lkk.{2}:;,", + "{5} :kkkkkdoxkkx", + "{5} ;kkkkl...", ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, @@ -452,8 +469,11 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c}████████ {w}████████ {w}████████", ], compact_lines: Some(&[ + "{c}||||||||| ||||", + "{c}||||||||| ||||", + "{c}|||| ||||", + "{c}|||| |||| ||||", "{c}|||| |||| ||||", - "{c}|||| ", "{c}|||| |||| ||||", "{c}|||| |||| ||||", ]), @@ -496,28 +516,32 @@ pub const ALL_LOGOS: &[Logo] = &[ name: "opensuse", raw_lines: &[ "{w} .;ldkO0000Okdl;.", - "{w} .;d00xl:^''''''^:ok00d;.", - "{w} .d00l' 'o00d.", - "{w} .d0Kd'{c} Okxol:;,. {w}:O0d.", - "{w} .d{c}0Kx. oOO00kkkxl:. {w}:EKx.", - "{w} .o{c}0Kx. l00000000000kx:. {w}.K0o.", - "{w} :{c}0Kx. l00000000000000000kx; {w}K0:", - "{w} .d{c}0Kx. l000000000000000000000d. {w}.EKx.", - "{w} .d{c}0Kx. o0000000000000000000000o {w}.EKx.", - "{w} :{c}0Kx. l000000000000000000000o {w}K0:", - "{w} .o{c}0Kx. l000000000000000000kx; {w}.K0o.", - "{w} .d{c}0Kx. oOO00kkkxl:. {w}:EKx.", - "{w} .d0Kd'{c} Okxol:;,. {w}:O0d.", - "{w} .d00l' 'o00d.", - "{w} .;d00xl:^''''''^:ok00d;.", - "{w} .;ldkO0000Okdl;.", + "{w} .;d00xl:^''''''^:ok00d;.", + "{w} .d00l' 'o00d.", + "{w} .d0Kd'{c} Okxol:;,. {w}:O0d", + "{w} .OK{c}KKK0kOKKKKKKKKKKOxo:, {w}lKO.", + "{w} ,0K{c}KKKKKKKKKKKKKKK0P^{w},,,{c}^dx:{w} ;00,", + "{w}.OK{c}KKKKKKKKKKKKKKKk'{w}.oOPPb.{c}'0k.{w} cKO.", + "{w}:KK{c}KKKKKKKKKKKKKKK: {w}kKx..dd {c}lKd{w} 'OK:", + "{w}dKK{c}KKKKKKKKKOx0KKKd {w}^0KKKO' {c}kKKc{w} dKd", + "{w}dKK{c}KKKKKKKKKK;.;oOKx,..{w}^{c}..;kKKK0.{w} dKd", + "{w}:KK{c}KKKKKKKKKK0o;...^cdxxOK0O/^^' {w}.0K:", + "{w} kKK{c}KKKKKKKKKKKKK0x;,,......,;od {w}lKk", + "{w} '0K{c}KKKKKKKKKKKKKKKKKKKK00KKOo^ {w}c00'", + "{w} 'kK{c}KKOxddxkOO00000Okxoc;'' {w}.dKk'", + "{w} l0Ko. .c00l'", + "{w} 'l0Kk:. .;xK0l'", + "{w} 'lkK0xl:;,,,,;:ldO0kl'", + "{w} '^:ldxkkkkxdl:^'", ], compact_lines: Some(&[ "{c} _______", - "{c} __| __ \\", - "{c}/ *\\ / \\", - "{c}\\__ _/", - "{c} \\_____/", + "{c}__| __ \\", + "{c} / .\\ \\", + "{c} \\__/ |", + "{c} _______|", + "{c} \\_______", + "{c}__________/", ]), distro_color: "\x1b[38;5;71m", outer_color: WHITE_COLOR, @@ -530,9 +554,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} /dddddddddddddddddddddddddddd/", "{w} +dddddddddddddddddddddddddddddd+", "{w} `sdddddddddddddddddddddddddddddddds`", - "{w} `yddddddddddddddddddddddddddddddddddy`", + "{w} `ydddddddddddd++hdddddddddddddddddddy`", + "{w}.hddddddddddd+` `+ddddh:-sdddddddddddh.", + "{w}hdddddddddd+` `+y: .sddddddddddh", + "{w}ddddddddh+` `//` `.` -sddddddddd", + "{w}ddddddh+` `/hddh/` `:s- -sddddddd", + "{w}ddddh+` `/+/dddddh/` `+s- -sddddd", + "{w}ddd+` `/o` :dddddddh/` `oy- .yddd", + "{w}hdddyo+ohddyosdddddddddho+oydddy++ohdddh", "{w}.hddddddddddddddddddddddddddddddddddddh.", - "{w}+dddddddddddddddddddddddddddddddddddddd+", "{w} `yddddddddddddddddddddddddddddddddddy`", "{w} `sdddddddddddddddddddddddddddddddds`", "{w} +dddddddddddddddddddddddddddddd+", @@ -541,11 +571,12 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} .hddddddddddddddddddddddh.", ], compact_lines: Some(&[ - "{c} /\\", - "{c} // \\", - "{c} // \\", - "{c}/// \\", - "{c}//' \\", + "{w} /\\ /\\", + "{c} // \\ \\", + "{c} // \\ \\", + "{c}/// \\ \\", + "{c}// \\ \\", + "{c} \\", ]), distro_color: "\x1b[38;5;32m", outer_color: WHITE_COLOR, @@ -602,13 +633,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} __.;=====;.__", ], compact_lines: Some(&[ - "{c} _______", - "{c} \\ ____)", - "{c} \\ \\", - "{c} \\ \\ __", - "{c} \\ \\ ( \\", - "{c} \\ \\_\\ \\", - "{c} \\______)", + "{c} _______", + "{c} _ \\______ -", + "{c}| \\ {w}___ {c}\\ |", + "{c}| | {w}/ \\ {c}| |", + "{c}| | {w}\\___/ {c}| |", + "{c}| \\______ \\_|", + "{c} -_______\\", ]), distro_color: "\x1b[38;5;35m", outer_color: WHITE_COLOR, @@ -675,11 +706,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ▝▀▀▀ ▀▀▀▀▘ {c}▀▀▀▘", ], compact_lines: Some(&[ - "{c} \\\\ //", - "{c} \\\\//", - "{w} == > < ==", - "{c} //\\\\", - "{c} // \\\\", + "{c} \\\\ {w}\\\\ //", + "{c} ==\\\\__{w}\\\\/ {c}//", + "{w} // {w}\\\\{c}//", + "{w}==// {c}//==", + "{w} //{c}\\\\{w}___{c}//", + "{w}// {c}/\\\\ {w}\\\\==", + "{c} // \\\\ {w}\\\\", ]), distro_color: "\x1b[38;5;75m", outer_color: WHITE_COLOR, @@ -710,11 +743,21 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} {c}.{w}", ], compact_lines: Some(&[ - "{c} ..............", - "{c} ...,", - "{c} .---'", - "{c} / ", - "{c} / ", + "{c} -#. #", + "{c} @###", + "{c} -######", + "{c} @#########", + "{c}=##. .#####", + "{c}## ## ##", + "{c}## ## #", + "{c}## @###", + "{c}##. ###", + "{c} ##% ##-", + "{c} -##% -*", + "{c} :*##+", + "{c} :*#*", + "{c} -#", + "{c} @", ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, @@ -921,28 +964,30 @@ pub const ALL_LOGOS: &[Logo] = &[ Logo { name: "windows7", raw_lines: &[ - "{w} ,.=:!!t3Z3z.,", - "{w} :tt:::tt333EE3", - "{w} Et:::ztt33EEEL{c} @Ee., ..,{w}", - "{w} ;tt:::tt333EE7{c} ;EEEEEEttttt33#{w}", - "{w} :Et:::zt333EEQ.{c} $EEEEEttttt33QL{w}", - "{w} it::::tt333EEF{c} @EEEEEEttttt33F{w}", - "{w} ;3=*^```\"*4EEV{c} :EEEEEEttttt33@.{w}", - "{c} ,.=::::!t=., {w}`{c} @EEEEEEtttz33QF", - "{c} ;::::::::zt33) \"4EEEtttji3P*", - "{c} :t::::::::tt33.:Z3z.. `` ,..g.", - "{c} i::::::::zt33F AEEEtttt::::ztF", - "{c} ;:::::::::t33V ;EEEttttt::::t3", - "{c} E::::::::zt33L @EEEtttt::::z3F", - "{c}{3=*^```\"*4E3) ;EEEtttt:::::tZ`", - "{c} ` :EEEEtttt::::z7", - "{c} \"VEzjt:;;z>*`", + "{4} ,.=:!!t3Z3z.,", + "{4} :tt:::tt333EE3", + "{4} Et:::ztt33EEEL{1} @Ee., ..,", + "{4} ;tt:::tt333EE7{1} ;EEEEEEttttt33#", + "{4} :Et:::zt333EEQ.{1} $EEEEEttttt33QL", + "{4} it::::tt333EEF{1} @EEEEEEttttt33F", + "{4} ;3=*^```\"*4EEV{1} :EEEEEEttttt33@.", + "{6} ,.=::::!t=., {4}`{1} @EEEEEEtttz33QF", + "{6} ;::::::::zt33){1} \"4EEEtttji3P*", + "{6} :t::::::::tt33.{2}:Z3z..{1} ``{2} ,..g.", + "{6} i::::::::zt33F{2} AEEEtttt::::ztF", + "{6} ;:::::::::t33V{2} ;EEEttttt::::t3", + "{6} E::::::::zt33L{2} @EEEtttt::::z3F", + "{6}{3=*^```\"*4E3){2} ;EEEtttt:::::tZ`", + "{6} `{2} :EEEEtttt::::z7", + "{2} \"VEzjt:;;z>*`", ], compact_lines: Some(&[ - "{1} ,. {2}.....", - "{1}$$$ {2}$$$$$", - "{4}$$$ {3}$$$$$", - "{4} `' {3}`````", + "{4} ,.=:!!t3Z. {1} @Ee.,", + "{4}:tt:::tt333 {1};EEEEtt33#", + "{4}Et:::ztt33E {1}@EEEEtt33F", + "{6};::::zt33) {2} AEEtttztF", + "{6}:t:::tt33. {2} ;EEEtt::3", + "{6} `\"*4E3) {2} :EEEEttz7", ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, @@ -971,11 +1016,12 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} ooss ssoo", ], compact_lines: Some(&[ - "{c} __ __", - "{c} | \\_/ |", - "{c} | {w}O O{c} |", - "{c} \\_______/", - "{c} | |", + "{c} ;, ,;", + "{c} ';,.-----.,;'", + "{c} ,' ',", + "{c} / {w}O O{c} \\", + "{c}| |", + "{c}'-----------------'", ]), distro_color: "\x1b[38;5;118m", outer_color: WHITE_COLOR, @@ -1016,27 +1062,38 @@ pub const ALL_LOGOS: &[Logo] = &[ Logo { name: "openbsd", raw_lines: &[ - "{w} _", - "{w} (_)", + "{6} _", + "{6} (_)", "{c} | .", - "{c} . |L /| . {w}_", - "{c} _ . |\\ _| \\--+._/| . {w}(_)", + "{c} . |L /| . {6} _", + "{c} _ . |\\ _| \\--+._/| . {6}(_)", "{c} / ||\\| Y J ) / |/| ./", - "{c} J |)'( | ` F`.'/ {w}_", - "{c} -<| F __ .-< {w}(_)", - "{c} | / .-'.`} . {'-.", - "{c} + | / /`L / | \\", - "{c} `\\ [ [ _ [ | ]", - "{c} \\__ \\._/ \\_/__/ ./", - "{c} `---._____.-'", + "{c} J |)'( | ` F`.'/ {6} _", + "{c} -<| F __ .-< {6}(_)", + "{c} | / .-'{6}. {c}`. /{6}-. {c}L___", + "{c} J \\ < {6}\\ {c} | | {6}O{6}\\{c}|.-' {6}_", + "{c} _J \\ .- \\{6}/ {6}O {6}| {c}| \\ |{c}F {6}(_)", + "{c} '-F -<_. \\ .-' `-' L__", + "{c}__J _ _. >-' {c}){4}._. {c}|-'", + "{c} `-|.' /_. {4}\\_| {c} F", + "{c} /.- . _.<", + "{c} /' /.' .' `\\", + "{c} /L /' |/ _.-'-\\", + "{c} /'J ___.---'\\|", + "{c} |\\ .--' V | `. `", + "{c} |/`. `-. `._)", + "{c} / .-.\\", + "{c} \\ ( `\\", + "{c} `.\\", ], compact_lines: Some(&[ - "{w} _.", - "{c} .-'{w}*{c}`-.", - "{c} .' {w}O O{c} `.", - "{c} : {w}(____){c} :", - "{c} `. .'", - "{c} `-...-'", + "{c} _____", + "{c} \\- -/", + "{c} \\_/ \\", + "{c} | {6}O O{c} |", + "{c} |_ < ) 3 )", + "{c} / \\ /", + "{c} /-_____-\\", ]), distro_color: "\x1b[38;5;220m", outer_color: WHITE_COLOR, @@ -1044,26 +1101,33 @@ pub const ALL_LOGOS: &[Logo] = &[ Logo { name: "netbsd", raw_lines: &[ - "{c} \\\\`-______,----__", - "{c} \\\\ __,---`._", - "{c} \\\\ `.____", - "{c} \\\\ `---.", - "{c} \\\\ \\\\", - "{c} \\\\ \\\\", - "{c} \\\\ {w}`-----.{c} \\\\", - "{c} \\\\ {w}/\\\\__\\\\{c} \\\\", - "{c} \\\\ {w}/ __ \\\\{c} \\\\", - "{c} \\\\{w}/ / \\\\ \\\\{c} \\\\", - "{c} {w}/ / \\\\ \\\\{c} \\\\", - "{c} {w}/ / \\\\ \\\\{c} \\\\", - "{c} {w}/__/{c} {w}\\\\__\\\\{c} \\\\", + "{c} `-/oshdmNMNdhyo+:-`", + "{w}y{c}/s+:-`` `.-:+oydNMMMMNhs/-``", + "{w}-m+{c}NMMMMMMMMMMMMMMMMMMMNdhmNMMMmdhs+/-`", + "{w} -m+{c}NMMMMMMMMMMMMMMMMMMMMmy+:`", + "{w} -N/{c}dMMMMMMMMMMMMMMMds:`", + "{w} -N/{c}hMMMMMMMMMmho:`", + "{w} -N/{c}-:/++/:.`", + "{w} :M+", + "{w} :Mo", + "{w} :Ms", + "{w} :Ms", + "{w} :Ms", + "{w} :Ms", + "{w} :Ms", + "{w} :Ms", + "{w} :Ms", + "{w} :Ms", ], compact_lines: Some(&[ - "{w}\\\\`-____", - "{c} \\\\", - "{c} \\\\", - "{c} \\\\", - "{c} \\\\", + "{w} \\`{c}-______,----__", + "{w} \\ {c} __,---`_", + "{w} \\ {c} `.____", + "{w} \\{c}-______,----`-", + "{w} \\", + "{w} \\", + "{w} \\", + "{w} \\", ]), distro_color: "\x1b[38;5;208m", outer_color: WHITE_COLOR, @@ -1095,12 +1159,17 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ..........................", ], compact_lines: Some(&[ - "{c} ____", - "{c} .' `.", - "{c} / __ \\", - "{c} | / \\ |", - "{c} \\ \\__/ /", - "{c} `.____.'", + "{c} ...........", + "{w} /,,,,{c}...../", + "{w} /,,,{c}..{w},,{c}../ {w}()", + "{w} /,,,,{c}../", + "{w} /,,,{c}.../ {c}/'\\", + "{w}/,{c}...../ {w}\\,/", + "{w}\\,,,,,,\\\\ {c}_", + "{w} \\,,{c}....\\\\ {c}/ \\", + "{c} \\...{w},{c}..\\\\ {c}\\_/", + "{c} \\..{w},,,,,,,,,,,,/", + "{c} \\{w},,{c}........{w},,/", ]), distro_color: "\x1b[38;5;43m", outer_color: WHITE_COLOR, @@ -1125,11 +1194,14 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c}██████████████████ █████████████", ], compact_lines: Some(&[ - "{c} /\\", - "{c} / \\", - "{c} / () \\", - "{c} / \\", - "{c} /________\\", + "{c}███████████████", + "{c}██ {w}██ {c}██", + "{c}██ {w}█████ █ {c}██", + "{c}██ {w}█ █ {c}██", + "{c}█████ {w}█ {c}██", + "{c}██ {w}████████ {c}██", + "{c}██ {w}██ {c}██", + "{c}████████ {w}█████", ]), distro_color: "\x1b[38;5;39m", outer_color: WHITE_COLOR, @@ -1157,11 +1229,11 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} .d988999889889899dd.", ], compact_lines: Some(&[ - "{c} /\\", - "{c} / \\", - "{c} / /\\ \\", - "{c} / / \\ \\", - "{c} /_/ \\_\\", + "{c} .----.", + "{c} .' {w},{c} '.", + "{c} .' {w}'-----|{c}", + "{c}'. {w}-----,{c}", + "{c} '.____.'", ]), distro_color: "\x1b[38;5;208m", outer_color: WHITE_COLOR, @@ -1230,11 +1302,15 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} -", ], compact_lines: Some(&[ - "{c} .---.", - "{c} / O \\", - "{c} | /| |", - "{c} \\ \\| /", - "{c} '---'", + "{c} `:oho/-`", + "{c} `mMMMMMMMMh-", + "{c} +M{w}o{c}hNMMMMMMm/", + "{c} .My .+dMMMMh.", + "{c} + :NMMNo", + "{c} `yMMm:", + "{c} /NMMy`", + "{c} .hMN+", + "{c} /", ]), distro_color: "\x1b[38;5;44m", outer_color: WHITE_COLOR, @@ -1242,33 +1318,35 @@ pub const ALL_LOGOS: &[Logo] = &[ Logo { name: "kubuntu", raw_lines: &[ - "{w} `.:/ossyyyysso/:.", - "{w} .:oyyyyyyyyyyyyyyyyyyo:`", - "{w} -oyyyyyyy{c}odMMyyyyyyyysyyyyo{w}-", - "{w} -syyyyyyyyyy{c}dMMyoyyyydmMMyyyyys{w}-", - "{w} oyyysdMysyyyy{c}dMMMMMMMMMMMMMyyyyyyy{w}o", - "{w} `oyyyydMMMMysyy{c}soooooodMMMMyyyyyyyy{w}o`", - "{w} oyyyyyydMMMMyyy{c}yyyyyyyyysdMMysssssyy{w}o", - "{w}-yyyyyyyydMysyyy{c}yyyyyyyyyysdMMMMMysyy{w}y-", - "{w}oyyyysoodMyyyyyy{c}yyyyyyyyyyyyydMMMMysyy{w}yo", - "{w}yyysdMMMMMyyyyyy{c}yyyyyyyyyyyyysosyyyyyy{w}yy", - "{w}yyysdMMMMMyyyyyy{c}yyyyyyyyyyyyyyyyyyyyyy{w}yy", - "{w}oyyyyysosdyyyyyy{c}yyyyyyyyyyyyydMMMMysyy{w}yo", - "{w}-yyyyyyyydMysyyy{c}yyyyyyyyyysdMMMMMysyy{w}y-", - "{w} oyyyyyydMMMysyy{c}yyyyyyyyysdMMyoyyyoyy{w}yo", - "{w} `oyyyydMMMysyyy{c}oooooodMMMMyoyyyyyyyy{w}o`", - "{w} oyyysyyoyyyys{c}dMMMMMMMMMMMyyyyyyyy{w}o", - "{w} -syyyyyyyyyd{c}MMMysyyydMMMysyyyys{w}-", - "{w} -oyyyyyyyd{c}MMyyyyyyysosyyyyo{w}-", - "{w} ./oyyyyy{c}yyyyyyyyyyyyyo{w}/.", - "{w} `.:/oosyyyysso/:.`", + "{c} `.:/ossyyyysso/:.", + "{c} .:oyyyyyyyyyyyyyyyyyyo:`", + "{c} -oyyyyyyy{w}odMMy{c}yyyyyyysyyyyo-", + "{c} -syyyyyyyyyy{w}dMMy{c}oyyyy{w}dmMMy{c}yyyys-", + "{c} oyyys{w}dMy{c}syyyy{w}dMMMMMMMMMMMMMy{c}yyyyyyo", + "{c} `oyyyy{w}dMMMMy{c}syysoooooo{w}dMMMMy{c}yyyyyyyyo`", + "{c} oyyyyyy{w}dMMMMy{c}yyyyyyyyyyys{w}dMMy{c}sssssyyyo", + "{c}-yyyyyyyy{w}dMy{c}syyyyyyyyyyyyyys{w}dMMMMMy{c}syyy-", + "{c}oyyyysoo{w}dMy{c}yyyyyyyyyyyyyyyyyy{w}dMMMMy{c}syyyo", + "{c}yyys{w}dMMMMMy{c}yyyyyyyyyyyyyyyyyysosyyyyyyyy", + "{c}yyys{w}dMMMMMy{c}yyyyyyyyyyyyyyyyyyyyyyyyyyyyy", + "{c}oyyyyysos{w}dy{c}yyyyyyyyyyyyyyyyyy{w}dMMMMy{c}syyyo", + "{c}-yyyyyyyy{w}dMy{c}syyyyyyyyyyyyyys{w}dMMMMMy{c}syyy-", + "{c} oyyyyyy{w}dMMMy{c}yyyyyyyyyyys{w}dMMy{c}oyyyoyyyo", + "{c} `oyyyy{w}dMMMy{c}syyyoooooo{w}dMMMMy{c}oyyyyyyyyo`", + "{c} oyyysyyoyyyys{w}dMMMMMMMMMMMy{c}yyyyyyyo", + "{c} -syyyyyyyyy{w}dMMMy{c}syyy{w}dMMMy{c}syyyys-", + "{c} -oyyyyyyy{w}dMMy{c}yyyyyysosyyyyo-", + "{c} ./oyyyyyyyyyyyyyyyyyyo/.", + "{c} `.:/oosyyyysso/:.`", ], compact_lines: Some(&[ - "{c} ___", - "{c} / \\", - "{c} | O |", - "{c} \\ /", - "{c} ---", + "{c} .:/::/::.", + "{c} / {w}|| //{c} `.", + "{c} |{w}== || //{c} |", + "{c} | {w}||<<{c} |", + "{c} |{w}== || \\\\{c} |", + "{c} \\ {w}|| \\\\{c} .'", + "{c} `:|::|::'", ]), distro_color: "\x1b[38;5;33m", outer_color: WHITE_COLOR, @@ -1295,11 +1373,11 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} ⠈⠙⠛⠛⠛⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⠛⠛⠛⠉⠁", ], compact_lines: Some(&[ - "{c} /\\", - "{c} / \\", - "{c} / /\\ \\", - "{c} / / \\ \\", - "{c} /_/ \\_\\", + "{w} ⢀⣤⣶⣤⣀", + "{w} ⣿⣿⣿⣿⣿⣷⣄", + "{w} ⣿⣿{c}⢠⣶⣄{w}⠈⢿⣿", + "{c} ⣿⣿{c}⠻⠿⠁{c} ⢸⣿", + "{c} ⠘⢿⣿⣶⣤⣤⣾⠟", ]), distro_color: "\x1b[38;5;197m", outer_color: WHITE_COLOR, @@ -1328,11 +1406,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{c} ###########", ], compact_lines: Some(&[ - "{c} .---.", - "{c} / B \\", - "{c} | B |", - "{c} \\ B /", - "{c} '---'", + "{w} %%% %%%%", + "{w} %%% %%%%{c}##", + "{w} = =={c}###", + "{w} %%% %{c}###==##", + "{w} %%{c}#####==##", + "{c} #######", + "{c} ###", ]), distro_color: "\x1b[38;5;141m", outer_color: WHITE_COLOR, @@ -1361,11 +1441,13 @@ pub const ALL_LOGOS: &[Logo] = &[ "{w} ''", ], compact_lines: Some(&[ - "{c} .---.", - "{c} / | \\", - "{c} | --+-- |", - "{c} \\ | /", - "{c} '---'", + " {1}____{2}^{3}____", + " {1}|\\ {2}|{3} /|", + " {1}| \\ {2}|{3} / |", + "{3}<---- {6}---->", + " {6}| / {1}|{2} \\ |", + " {6}|/__{1}|{2}__\\|", + " {1}v", ]), distro_color: "\x1b[38;5;214m", outer_color: WHITE_COLOR, From 218f50ab7e14beb3d59c10d781634ae42a881ef3 Mon Sep 17 00:00:00 2001 From: Kushagra Kumar Date: Sat, 7 Nov 2026 06:00:42 +0530 Subject: [PATCH 4/4] docs(readme): document adaptive ascii logos and update benchmarks --- README.md | 99 +++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 71 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 5139549..98a7ca9 100644 --- a/README.md +++ b/README.md @@ -2,48 +2,87 @@ A fast, lightweight, zero-subprocess system information fetch tool written in Rust for Linux, Windows, macOS, and Android. +### Standard Full Logo + ```text -╭─ ~ at 01:36 +╭─ ~ at 05:50 ╰─❯ kkfetch .',;::::;,'. kk376@fedora .';:cccccccccccc:;,. ------------ - .;cccccccccccccccccccccc;. OS: Fedora Linux 44 (Workstation Edition) x86_64 + .;cccccccccccccccccccccc;. OS: Fedora Linux 45 (Workstation Edition) x86_64 .:cccccccccccccccccccccccccc:. Host: Thin A15 B7UCX REV:1.0 - .;ccccccccccccc;.:dddl:.;ccccccc;. Kernel: 7.2.13-200.fc44.x86_64 - .:ccccccccccccc;OWMKOOXMWd;ccccccc:. Installed: 15 Oct 2026, 08:58 PM (16 days ago) -.:ccccccccccccc;KMMc;cc;xMMc;ccccccc:. Uptime: 3 hours, 16 mins -,cccccccccccccc;MMM.;cc;;WW:;cccccccc, Packages: 1892 (rpm), 4 (flatpak), 1 (npm) + .;ccccccccccccc;.:dddl:.;ccccccc;. Kernel: 7.2.15-300.fc45.x86_64 + .:ccccccccccccc;OWMKOOXMWd;ccccccc:. Installed: 15 Oct 2026, 08:58 PM (23 days ago) +.:ccccccccccccc;KMMc;cc;xMMc;ccccccc:. Uptime: 2 hours, 44 mins +,cccccccccccccc;MMM.;cc;;WW:;cccccccc, Packages: 1904 (rpm), 6 (flatpak), 1 (npm) :cccccccccccccc;MMM.;cccccccccccccccc: Shell: zsh 5.9 :ccccccc;oxOOOo;MMM0OOk.;cccccccccccc: Display (AUOD0A2): 1920x1080 @ 1.25x in 15", 144 Hz [Built-in] cccccc;0MMKxdd:;MMMkddc.;cccccccccccc; WM: Mutter (Wayland) ccccc;XM0';cccc;MMM.;cccccccccccccccc' WM Theme: Adwaita ccccc;MMo;ccccc;MMW.;ccccccccccccccc; Desktop: GNOME 50.6 -ccccc;0MNc.ccc.xMMd;ccccccccccccccc; Terminal: kitty 0.47.1 -cccccc;dNMWXXXWM0:;cccccccccccccc:, Terminal Font: family="FiraCode Nerd Font" (14pt) +ccccc;0MNc.ccc.xMMd;ccccccccccccccc; Terminal: Ghostty 1.4.0-1.fc45 +cccccc;dNMWXXXWM0:;cccccccccccccc:, Terminal Font: FiraCode Nerd Font (13pt) cccccccc;.:odl:.;cccccccccccccc:,. Theme: Adwaita (dark) [GTK/GNOME] :cccccccccccccccccccccccccccc:'. Icons: Adwaita [GTK/GNOME] .:cccccccccccccccccccccc:;,.. Cursor: Adwaita (24px) [GTK/GNOME] - '::cccccccccccccc::;,. CPU: AMD Ryzen 5 7535HS (6c 12t) @ 2.958GHz [4.60GHz max] + '::cccccccccccccc::;,. CPU: AMD Ryzen 5 7535HS (6c 12t) @ 4.340GHz [4.60GHz max] GPU0: AMD Radeon 660M (512 MiB) [Integrated] GPU1: NVIDIA GeForce RTX 2050 (4 GiB) [Discrete] - Memory: 1.67 GiB / 14.82 GiB (11%) - Swap: 0.00 GiB / 14.82 GiB (0%) - ZSTD - Disk0: (/) 27.4 GiB / 474.4 GiB (6%) - btrfs - Disk1: (/mnt/Backup) 463.9 GiB / 931.5 GiB (50%) - fuseblk - Battery: 98% [AC Connected] + Memory: 7.99 GiB / 14.82 GiB (54%) + Swap: 0.81 GiB / 14.82 GiB (5%) - ZSTD + Disk0: (/) 112.1 GiB / 474.4 GiB (24%) - btrfs + Disk1: (/mnt/Backup) 495.2 GiB / 931.5 GiB (53%) - fuseblk + Battery: 96% [AC Connected] Local IP (Ethernet): 192.168.29.21/24 ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ``` +### Adaptive Compact Logo + +```text +╭─ ~ at 05:50 +╰─❯ kkfetch --compact-logo + ,'''''. kk376@fedora + | ,. | ------------ + | | '_' OS: Fedora Linux 45 (Workstation Edition) x86_64 + ,....| |.. Host: Thin A15 B7UCX REV:1.0 +.' ,_;| ..' Kernel: 7.2.15-300.fc45.x86_64 +| | | | Installed: 15 Oct 2026, 08:58 PM (23 days ago) +| ',_,' | Uptime: 2 hours, 44 mins + '. ,' Packages: 1904 (rpm), 6 (flatpak), 1 (npm) + ''''' Shell: zsh 5.9 + Display (AUOD0A2): 1920x1080 @ 1.25x in 15", 144 Hz [Built-in] + WM: Mutter (Wayland) + WM Theme: Adwaita + Desktop: GNOME 50.6 + Terminal: Ghostty 1.4.0-1.fc45 + Terminal Font: FiraCode Nerd Font (13pt) + Theme: Adwaita (dark) [GTK/GNOME] + Icons: Adwaita [GTK/GNOME] + Cursor: Adwaita (24px) [GTK/GNOME] + CPU: AMD Ryzen 5 7535HS (6c 12t) @ 4.020GHz [4.60GHz max] + GPU0: AMD Radeon 660M (512 MiB) [Integrated] + GPU1: NVIDIA GeForce RTX 2050 (4 GiB) [Discrete] + Memory: 8.00 GiB / 14.82 GiB (54%) + Swap: 0.81 GiB / 14.82 GiB (5%) - ZSTD + Disk0: (/) 112.1 GiB / 474.4 GiB (24%) - btrfs + Disk1: (/mnt/Backup) 495.2 GiB / 931.5 GiB (53%) - fuseblk + Battery: 96% [AC Connected] + Local IP (Ethernet): 192.168.29.21/24 + ███ ███ ███ ███ ███ ███ ███ ███ + ███ ███ ███ ███ ███ ███ ███ ███ +``` + --- ## Why KKFetch? Most fetch tools either spawn multiple shell child processes (`neofetch`) or dynamically link heavy C runtime libraries (`fastfetch`). KKFetch is built with a different design philosophy: -* **Sub-3ms Latency**: Queries virtual filesystems (`/proc`, `/sys`), POSIX syscalls, and Win32 APIs directly with zero child process spawning (`fork`/`execve`). In statistical benchmarks, it is **6.1x faster than Fastfetch** on raw data collection across all 27 active modules. -* **Native OS Install Date**: Probes root filesystem creation timestamp (`statx` birth time) and installer logs, showing exact installation date and relative age (`6 days ago`). +* **Sub-2ms Latency (Sub-1ms Internal Engine)**: Queries virtual filesystems (`/proc`, `/sys`), POSIX syscalls, and Win32 APIs directly with zero child process spawning (`fork`/`execve`). In statistical benchmarks, it is **13.2x faster than Fastfetch** end-to-end across all active modules. +* **Responsive Adaptive ASCII Engine**: Zero-dependency constraint solver dynamically evaluates terminal window geometry and selects between Side-by-Side (Full), Side-by-Side (Compact), Stacked, and Info-Only layouts with zero line-wrapping distortion. Includes an automatic 3-tier ANSI fallback engine (TrueColor -> 256-color -> 16-color ANSI) for Linux TTY consoles and serial terminals. +* **Native OS Install Date**: Probes root filesystem creation timestamp (`statx` birth time) and installer logs, showing exact installation date and relative age (`23 days ago`). * **GPU Memory & Classification**: Probes dedicated VRAM and classifies graphics hardware into `[Integrated]` and `[Discrete]` tiers with sequential indexing. * **Zero-Fork Display EDID Parsing**: Probes monitor name, refresh rate, physical diagonal size, and panel type directly from DRM sysfs without spawning `xrandr` or display server queries. * **Declarative Configuration & Plugins**: Supports `~/.config/kkfetch/config.toml` and custom executable plugin modules with parallel execution. @@ -53,15 +92,16 @@ Most fetch tools either spawn multiple shell child processes (`neofetch`) or dyn ## Benchmarks -Benchmarked against Fastfetch across **100 iterations** (20 warmup runs) on bare-metal Fedora Linux 44 (Linux 7.2.7-200.fc44.x86_64, AMD Ryzen 5 7535HS with 6 physical cores and 12 threads): +Benchmarked against Fastfetch across **100 iterations** (20 warmup runs) on bare-metal Fedora Linux 45 (Linux 7.2.15-300.fc45.x86_64, AMD Ryzen 5 7535HS with 6 physical cores and 12 threads): ### Results | Command | Mean Runtime | Median Latency | Min Latency | Max Latency | Relative Speedup | | :--- | :---: | :---: | :---: | :---: | :---: | -| `fastfetch` | `14.56 ms` | `13.26 ms` | `10.74 ms` | `114.27 ms` | `1.00` (Baseline) | -| `kkfetch` (All 27 Modules) | **`2.37 ms`** | **`2.34 ms`** | **`1.79 ms`** | **`2.93 ms`** | **6.14x faster** | -| `kkfetch` (Pure sysfs/drm) | **`2.35 ms`** | **`2.32 ms`** | **`1.78 ms`** | **`2.90 ms`** | **6.20x faster** | +| `fastfetch` | `26.94 ms` | `24.70 ms` | `23.70 ms` | `124.86 ms` | `1.00` (Baseline) | +| `kkfetch` (Standard Full Logo) | **`2.04 ms`** | **`2.01 ms`** | **`1.73 ms`** | **`2.50 ms`** | **13.21x faster** | +| `kkfetch` (`--compact-logo`) | **`2.04 ms`** | **`2.03 ms`** | **`1.76 ms`** | **`2.49 ms`** | **13.21x faster** | +| `kkfetch` (Internal Engine via `--timings`) | **`1.08 ms`** | **`0.77 ms`** | **`0.70 ms`** | **`1.14 ms`** | **Sub-1ms Engine** | *KKFetch achieves lower CPU time and syscall overhead by reading `/proc` and `sysfs` directly in Rust, executing active module collectors concurrently in parallel using `std::thread::scope`, and compiling with Fat Link-Time Optimization (LTO).* @@ -80,7 +120,7 @@ hyperfine --warmup 20 --runs 100 -N 'kkfetch' 'fastfetch' ``` **3. Pure Sysfs / Accelerated Mode Benchmark:** -Measures the core sub-3ms performance of KKFetch with external bus latency disabled, highlighting zero-fork `/proc`, `sysfs`, and DRM kernel prober throughput against Fastfetch: +Measures the core sub-2ms performance of KKFetch with external bus latency disabled, highlighting zero-fork `/proc`, `sysfs`, and DRM kernel prober throughput against Fastfetch: ```bash hyperfine --warmup 20 --runs 100 -N 'kkfetch -d battery' 'fastfetch' ``` @@ -89,20 +129,20 @@ hyperfine --warmup 20 --runs 100 -N 'kkfetch -d battery' 'fastfetch' ## Supported Operating Systems & Logos -KKFetch includes high-contrast ASCII art logos with distro brand signature colors for **31 operating systems, platforms, and mascots**: +KKFetch includes high-contrast ASCII art logos with distro brand signature colors and adaptive compact variants for **40 operating systems, platforms, and mascots**: | Family / Ecosystem | Supported Distributions & Targets | | :--- | :--- | -| **Debian / Ubuntu Family** | Ubuntu, Debian, Linux Mint, Pop!_OS, Kali Linux, Zorin OS (6) | -| **Fedora / Red Hat Family** | Fedora 43, 44, 45, Rawhide, RHEL, Rocky Linux, AlmaLinux (4) | -| **openSUSE** | openSUSE Tumbleweed, openSUSE Leap 15.6 (1) | +| **Debian / Ubuntu Family** | Ubuntu, Debian, Kubuntu, Linux Mint, Pop!_OS, Kali Linux, Parrot Security, Zorin OS (8) | +| **Fedora / Red Hat Family** | Fedora (43, 44, 45, Rawhide), RHEL, Rocky Linux, AlmaLinux, CentOS Stream, Nobara Linux (6) | +| **Arch Family** | Arch Linux, EndeavourOS, Manjaro, Artix Linux, CachyOS, Garuda Linux, BlackArch, Omarchy (8) | +| **openSUSE** | openSUSE Tumbleweed, openSUSE Leap 15.6, Slowroll (1) | | **NixOS / Nix** | NixOS, Nix Flakes (1) | | **Windows** | Windows 11, Windows 10, Windows 7 (native Win32 x86_64) (3) | -| **Arch Family** | Arch Linux, EndeavourOS, Manjaro, Artix Linux (4) | | **macOS** | macOS (Apple Silicon aarch64 & Intel x86_64) (1) | | **Android / Mobile** | Android (via Termux aarch64 & x86_64) (1) | -| **Alpine & Independent Linux** | Alpine Linux, Gentoo Linux, Void Linux, Vylen Linux, Slackware (5) | -| **BSD Family & Mascots** | FreeBSD, OpenBSD, NetBSD, Ferris the Rust Crab (`ferris`), Linux Penguin (`tux`) (5) | +| **Alpine & Independent Linux** | Alpine Linux, Gentoo Linux, Void Linux, Vylen Linux, Bazzite, Slackware (6) | +| **BSD Family & Mascots** | FreeBSD, OpenBSD, NetBSD, Ferris the Rust Crab (`ferris`), Linux Penguin (`tux` / `generic`) (5) | --- @@ -263,8 +303,11 @@ sudo cp target/release/kkfetch /usr/local/bin/ | `-m, --modules ` | Select and order specific modules (e.g. `os,kernel,cpu,memory`) | | `-d, --disable ` | Disable specific modules from output (e.g. `gpu,disk`) | | `-l, --logo ` | Override ASCII logo (e.g. `arch`, `debian`, `ferris`, `ubuntu`, `fedora`, `tux`, `none`) | +| `--logo-variant ` | Select ASCII logo variant: `auto` (adaptive constraint solver), `full`, or `compact` | +| `--compact-logo` | Force compact ASCII logo layout (alias: `--compact`) | | `--no-logo` | Suppress the ASCII logo and print only system telemetry | | `--no-color` | Disable ANSI color escapes | +| `--no-plugins` | Disable external plugin discovery and execution | | `--disk-path ` | Target filesystem path for disk statistics (default: `/`) | | `--list-modules` | Print all available information modules and exit | | `--json` | Output system information in structured JSON format |