#[cfg(feature = "graphics")]
pub fn get_embedded_logo(distro: Option<&str>) -> Option<&'static [u8]> {
let d = distro.map(|s| s.to_lowercase());
match d.as_deref() {
Some("arch") => Some(include_bytes!("../assets/logos/arch.png")),
Some("debian") => Some(include_bytes!("../assets/logos/debian.png")),
Some("fedora") => Some(include_bytes!("../assets/logos/fedora.png")),
Some("nixos") => Some(include_bytes!("../assets/logos/nixos.png")),
Some("ubuntu") => Some(include_bytes!("../assets/logos/ubuntu.png")),
Some("pop") => Some(include_bytes!("../assets/logos/pop.png")),
Some("manjaro") => Some(include_bytes!("../assets/logos/manjaro.png")),
Some("endeavouros") => Some(include_bytes!("../assets/logos/endeavouros.png")),
Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
Some(include_bytes!("../assets/logos/opensuse.png"))
}
Some("mx") => Some(include_bytes!("../assets/logos/mx.png")),
Some("linuxmint") => Some(include_bytes!("../assets/logos/linuxmint.png")),
Some("kali") => Some(include_bytes!("../assets/logos/kali.png")),
Some("zorin") => Some(include_bytes!("../assets/logos/zorin.png")),
Some("garuda") => Some(include_bytes!("../assets/logos/garuda.png")),
Some("macos") => Some(include_bytes!("../assets/logos/macos.png")),
Some("windows") => Some(include_bytes!("../assets/logos/windows.png")),
_ => Some(include_bytes!("../assets/logos/tux.png")),
}
}
#[cfg(not(feature = "graphics"))]
pub fn get_embedded_logo(_distro: Option<&str>) -> Option<&'static [u8]> {
Some(include_bytes!("../assets/logos/tux.png"))
}
pub fn detect_distro() -> Option<String> {
#[cfg(target_os = "macos")]
{
Some("macos".to_string())
}
#[cfg(target_os = "windows")]
{
Some("windows".to_string())
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
if let Ok(content) = std::fs::read_to_string("/etc/os-release") {
for line in content.lines() {
if line.starts_with("ID=") {
let id = line.trim_start_matches("ID=").trim_matches('"');
return Some(id.to_string());
}
}
}
None
}
}
pub fn get_ascii_logo(distro: Option<&str>) -> Vec<String> {
let d = distro.map(|s| s.to_lowercase());
match d.as_deref() {
Some("arch") => {
let logo = include_str!("../assets/logos/arch.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("debian") => {
let logo = include_str!("../assets/logos/debian.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("fedora") => {
let logo = include_str!("../assets/logos/fedora.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("nixos") => {
let logo = include_str!("../assets/logos/nixos.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("ubuntu") => {
let logo = include_str!("../assets/logos/ubuntu.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("pop") => {
let logo = include_str!("../assets/logos/pop.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("manjaro") => {
let logo = include_str!("../assets/logos/manjaro.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("endeavouros") => {
let logo = include_str!("../assets/logos/endeavouros.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
let logo = include_str!("../assets/logos/opensuse.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("mx") => {
let logo = include_str!("../assets/logos/mx.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("linuxmint") => {
let logo = include_str!("../assets/logos/linuxmint.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("kali") => {
let logo = include_str!("../assets/logos/kali.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("zorin") => {
let logo = include_str!("../assets/logos/zorin.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("garuda") => {
let logo = include_str!("../assets/logos/garuda.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("macos") => {
let logo = include_str!("../assets/logos/macos.txt");
logo.lines().map(|s| s.to_string()).collect()
}
Some("windows") => {
let logo = include_str!("../assets/logos/windows.txt");
logo.lines().map(|s| s.to_string()).collect()
}
_ => {
let logo = include_str!("../assets/logos/tux.txt");
logo.lines().map(|s| s.to_string()).collect()
}
}
}
pub fn get_distro_colors(distro: Option<&str>) -> Vec<&'static str> {
let d = distro.map(|s| s.to_lowercase());
match d.as_deref() {
Some("arch") => vec!["\x1b[36m", "\x1b[37m"],
Some("debian") => vec!["\x1b[31m", "\x1b[37m"],
Some("fedora") => vec!["\x1b[34m", "\x1b[37m"],
Some("nixos") => vec![
"\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m", "\x1b[34m", "\x1b[36m",
],
Some("ubuntu") => vec!["\x1b[33m", "\x1b[31m"],
Some("pop") => vec!["\x1b[36m", "\x1b[37m"],
Some("manjaro") => vec!["\x1b[32m"],
Some("endeavouros") => vec!["\x1b[35m", "\x1b[31m", "\x1b[34m"],
Some("opensuse") | Some("opensuse-leap") | Some("opensuse-tumbleweed") => {
vec!["\x1b[32m", "\x1b[37m"]
}
Some("mx") => vec!["\x1b[34m", "\x1b[37m"],
Some("linuxmint") => vec!["\x1b[32m", "\x1b[37m"],
Some("kali") => vec!["\x1b[34m", "\x1b[37m"],
Some("zorin") => vec!["\x1b[36m", "\x1b[37m"],
Some("garuda") => vec!["\x1b[35m", "\x1b[36m"],
Some("macos") => vec![
"\x1b[38;5;252m",
"\x1b[38;5;250m",
"\x1b[38;5;248m",
"\x1b[38;5;246m",
"\x1b[38;5;244m",
],
Some("windows") => vec!["\x1b[36m"],
_ => vec!["\x1b[30m", "\x1b[37m", "\x1b[33m"], }
}
pub fn get_distro_logo_lines(distro: Option<&str>) -> Vec<String> {
let raw_lines = get_ascii_logo(distro);
let colors = get_distro_colors(distro);
let default_color = colors.first().copied().unwrap_or("\x1b[0m");
raw_lines
.into_iter()
.map(|line| {
let mut formatted = line;
for i in 1..=9 {
let color_val = colors.get(i - 1).copied().unwrap_or("\x1b[0m");
let placeholder = format!("${{{}}}", i);
formatted = formatted.replace(&placeholder, color_val);
let placeholder_short = format!("${}", i);
formatted = formatted.replace(&placeholder_short, color_val);
}
if !formatted.is_empty() {
format!("{}{}\x1b[0m", default_color, formatted)
} else {
formatted
}
})
.collect()
}
fn is_rio_terminal() -> bool {
if let Ok(term) = std::env::var("TERM") {
if term == "rio" || term.starts_with("xterm-rio") {
return true;
}
}
std::env::var("TERM_PROGRAM")
.map(|t| t == "rio")
.unwrap_or(false)
}
pub fn supports_kitty() -> bool {
std::env::var("TERM")
.map(|t| t == "xterm-kitty")
.unwrap_or(false)
|| std::env::var("TERMINAL_EMULATOR")
.map(|t| t == "iterm-kitty" || t == "iTerm.app")
.unwrap_or(false)
|| is_rio_terminal()
}
pub fn supports_iterm2() -> bool {
if let Ok(prog) = std::env::var("TERM_PROGRAM") {
if prog == "iTerm.app" || prog == "WezTerm" {
return true;
}
}
is_rio_terminal()
}
pub fn supports_sixel() -> bool {
if let Ok(term) = std::env::var("TERM") {
let term = term.to_lowercase();
if term.contains("sixel") || term.contains("foot") || term.contains("mlterm") {
return true;
}
}
if let Ok(prog) = std::env::var("TERM_PROGRAM") {
if prog == "WezTerm" || prog == "iTerm.app" {
return true;
}
}
if std::env::var("WT_SESSION").is_ok() {
return true;
}
is_rio_terminal()
}
pub const LOGO_MAX_COLS: usize = 45;
pub const LOGO_MAX_ROWS: usize = 10;
pub fn fit_logo_cells(
img_w: u32,
img_h: u32,
cell_w: usize,
cell_h: usize,
max_cols: usize,
max_rows: usize,
) -> LogoFit {
let (max_cols, max_rows) = (max_cols.max(1), max_rows.max(1));
if img_w == 0 || img_h == 0 || cell_w == 0 || cell_h == 0 {
return LogoFit {
cols: max_cols,
rows: max_rows,
width_limited: true,
};
}
let (img_w, img_h) = (u64::from(img_w), u64::from(img_h));
let box_w = (max_cols * cell_w) as u64;
let box_h = (max_rows * cell_h) as u64;
let width_limited = box_w * img_h <= box_h * img_w;
let (disp_w, disp_h) = if width_limited {
(box_w, box_w * img_h / img_w) } else {
(box_h * img_w / img_h, box_h) };
LogoFit {
cols: (disp_w as usize).div_ceil(cell_w).clamp(1, max_cols),
rows: (disp_h as usize).div_ceil(cell_h).clamp(1, max_rows),
width_limited,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LogoFit {
pub cols: usize,
pub rows: usize,
pub width_limited: bool,
}
pub fn kitty_placement_spec(fit: LogoFit) -> String {
if fit.width_limited {
format!("c={}", fit.cols)
} else {
format!("r={}", fit.rows)
}
}
pub fn terminal_cell_size_px() -> (usize, usize) {
#[cfg(unix)]
{
use std::mem::MaybeUninit;
let mut ws: libc::winsize = unsafe { MaybeUninit::zeroed().assume_init() };
let ret = unsafe { libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws) };
if ret == 0 && ws.ws_row > 0 && ws.ws_col > 0 && ws.ws_xpixel > 0 && ws.ws_ypixel > 0 {
return (
ws.ws_xpixel as usize / ws.ws_col as usize,
ws.ws_ypixel as usize / ws.ws_row as usize,
);
}
}
(10, 20)
}
pub fn logo_cells_for(img_w: u32, img_h: u32) -> LogoFit {
let (cell_w, cell_h) = terminal_cell_size_px();
fit_logo_cells(img_w, img_h, cell_w, cell_h, LOGO_MAX_COLS, LOGO_MAX_ROWS)
}
pub fn chafa_size_arg() -> String {
format!("{LOGO_MAX_COLS}x{LOGO_MAX_ROWS}")
}
static CHAFA_SUPPORTS_PROBE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
pub fn chafa_supports_probe() -> bool {
*CHAFA_SUPPORTS_PROBE.get_or_init(|| {
std::process::Command::new("chafa")
.args(["--probe", "off", "--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
})
}
pub fn chafa_available() -> bool {
std::process::Command::new("chafa")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn write_temp_logo(bytes: &[u8]) -> std::io::Result<std::path::PathBuf> {
let temp_path = std::env::temp_dir().join(format!("retch_logo_{}.png", std::process::id()));
std::fs::write(&temp_path, bytes)?;
Ok(temp_path)
}
pub fn print_with_chafa(path: &std::path::Path) -> bool {
let mut cmd = std::process::Command::new("chafa");
cmd.arg("--format")
.arg("symbols")
.arg("--size")
.arg(chafa_size_arg());
if chafa_supports_probe() {
cmd.arg("--probe").arg("off");
}
let output = cmd.arg(path).output();
match output {
Ok(out) if out.status.success() => {
print!("{}", String::from_utf8_lossy(&out.stdout));
true
}
Ok(out) => {
eprintln!("warning: chafa failed with status: {}", out.status);
false
}
Err(e) => {
eprintln!("warning: failed to execute chafa: {}", e);
false
}
}
}
pub fn get_chafa_logo_lines(path: &std::path::Path) -> Option<Vec<String>> {
let mut cmd = std::process::Command::new("chafa");
cmd.arg("--format")
.arg("symbols")
.arg("--size")
.arg(chafa_size_arg());
if chafa_supports_probe() {
cmd.arg("--probe").arg("off");
}
let output = cmd.arg(path).output().ok()?;
if output.status.success() {
let content = String::from_utf8_lossy(&output.stdout);
Some(content.lines().map(|s| s.to_string()).collect())
} else {
None
}
}
pub fn print_distro_logo(distro: Option<&str>) {
print_distro_logo_with_ascii(distro, false, false);
}
pub fn print_distro_logo_with_ascii(distro: Option<&str>, ascii_only: bool, chafa_only: bool) {
if ascii_only {
let art = get_distro_logo_lines(distro);
for line in art {
println!("{}", line);
}
return;
}
let has_chafa = chafa_available();
if !chafa_only {
let supports_kitty = supports_kitty();
let supports_iterm2 = supports_iterm2();
let supports_sixel = supports_sixel();
#[cfg(feature = "graphics")]
if supports_kitty {
if let Some(bytes) = get_embedded_logo(distro) {
if !bytes.is_empty() {
print_graphical_logo(bytes);
return;
}
}
}
#[cfg(feature = "graphics")]
if supports_iterm2 {
if let Some(bytes) = get_embedded_logo(distro) {
if !bytes.is_empty() {
print_iterm2_logo(bytes);
return;
}
}
}
#[cfg(feature = "graphics")]
if supports_sixel {
if let Some(bytes) = get_embedded_logo(distro) {
if !bytes.is_empty() {
print_sixel_logo(bytes);
return;
}
}
}
}
if has_chafa {
if let Some(bytes) = get_embedded_logo(distro) {
if bytes.len() > 100 {
if let Ok(temp_path) = write_temp_logo(bytes) {
if print_with_chafa(&temp_path) {
let _ = std::fs::remove_file(&temp_path);
return;
}
let _ = std::fs::remove_file(&temp_path);
}
}
}
}
let art = get_distro_logo_lines(distro);
for line in art {
println!("{}", line);
}
}
#[cfg(feature = "graphics")]
pub fn print_iterm2_logo(image_data: &[u8]) {
use base64::Engine;
let (width, height) = image::load_from_memory(image_data)
.map(|img| (img.width(), img.height()))
.unwrap_or((0, 0));
let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
if width > 0 && height > 0 {
let fit = logo_cells_for(width, height);
print!(
"\x1b]1337;File=inline=1;width={};height={};preserveAspectRatio=1:{}\x07",
fit.cols, fit.rows, encoded
);
} else {
print!(
"\x1b]1337;File=inline=1;height={};preserveAspectRatio=1:{}\x07",
LOGO_MAX_ROWS, encoded
);
}
println!(); }
#[cfg(feature = "graphics")]
pub fn print_iterm2_logo_from_path(path: &std::path::Path) {
if let Ok(bytes) = std::fs::read(path) {
print_iterm2_logo(&bytes);
} else {
println!("[Could not read logo for iTerm2 from {}]", path.display());
}
}
#[cfg(not(feature = "graphics"))]
pub fn print_iterm2_logo(_image_data: &[u8]) {
println!("[iTerm2 logo support requires --features graphics]");
}
#[cfg(feature = "graphics")]
pub fn print_graphical_logo(image_data: &[u8]) {
use base64::Engine;
let (width, height) = image::load_from_memory(image_data)
.map(|img| (img.width(), img.height()))
.unwrap_or((0, 0));
let encoded = base64::engine::general_purpose::STANDARD.encode(image_data);
if width > 0 && height > 0 {
let spec = kitty_placement_spec(logo_cells_for(width, height));
println!(
"\x1b_Gf=100,s={},v={},{},a=T;{}\x1b\\",
width, height, spec, encoded
);
} else {
println!("\x1b_Gf=100,a=T;{}", encoded);
}
}
#[cfg(feature = "graphics")]
pub fn print_sixel_logo(image_data: &[u8]) {
if let Ok(img) = image::load_from_memory(image_data) {
let fit = logo_cells_for(img.width(), img.height());
let (cell_w, cell_h) = terminal_cell_size_px();
let resized = img.resize(
(fit.cols * cell_w) as u32,
(fit.rows * cell_h) as u32,
image::imageops::FilterType::Triangle,
);
let rgba = resized.to_rgba8();
let (width, height) = rgba.dimensions();
print_sixel_rgba(rgba.as_raw(), width, height);
}
}
#[cfg(feature = "graphics")]
pub fn print_sixel_rgba(rgba: &[u8], width: u32, height: u32) {
use icy_sixel::SixelImage;
match SixelImage::try_from_rgba(rgba.to_vec(), width as usize, height as usize) {
Ok(sixel_img) => match sixel_img.encode() {
Ok(sixel_str) => {
print!("{}", sixel_str);
}
Err(e) => eprintln!("[Sixel Encoding Error: {}]", e),
},
Err(e) => eprintln!("[Sixel Creation Error: {}]", e),
}
}
#[cfg(not(feature = "graphics"))]
pub fn print_graphical_logo(_image_data: &[u8]) {
println!("[Graphical logo support requires --features graphics]");
}
#[cfg(not(feature = "graphics"))]
pub fn print_sixel_logo(_image_data: &[u8]) {
println!("[Sixel logo support requires --features graphics]");
}
#[cfg(feature = "graphics")]
pub fn print_graphical_logo_from_path(path: &std::path::Path) {
use image::ImageFormat;
match image::open(path) {
Ok(img) => {
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
let mut png_data = Vec::new();
if resized
.write_to(&mut std::io::Cursor::new(&mut png_data), ImageFormat::Png)
.is_ok()
{
print_graphical_logo(&png_data);
} else {
println!("[Failed to encode logo as PNG]");
}
}
Err(_) => {
println!("[Could not load graphical logo from {}]", path.display());
}
}
}
#[cfg(feature = "graphics")]
pub fn print_sixel_logo_from_path(path: &std::path::Path) {
match image::open(path) {
Ok(img) => {
let resized = img.resize(128, 128, image::imageops::FilterType::Lanczos3);
let rgba = resized.to_rgba8();
let (width, height) = rgba.dimensions();
print_sixel_rgba(rgba.as_raw(), width, height);
}
Err(_) => {
println!("[Could not load logo for Sixel from {}]", path.display());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_ascii_logo_arch() {
let logo = get_ascii_logo(Some("arch"));
assert!(!logo.is_empty());
assert!(logo[0].contains("`"));
}
#[test]
fn test_get_ascii_logo_unknown() {
let logo = get_ascii_logo(Some("unknown_distro"));
assert!(!logo.is_empty());
assert!(logo
.iter()
.any(|line| line.contains("o${2}_${3}o") || line.contains("o_o")));
}
#[test]
fn test_get_ascii_logo_none() {
let logo = get_ascii_logo(None);
assert!(!logo.is_empty());
}
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
struct EnvGuard {
_mutex_guard: std::sync::MutexGuard<'static, ()>,
old_vars: std::collections::HashMap<&'static str, Option<String>>,
}
impl EnvGuard {
fn new(vars_to_mock: &[&'static str]) -> Self {
let guard = ENV_LOCK.lock().unwrap();
let mut old_vars = std::collections::HashMap::new();
for var in vars_to_mock {
old_vars.insert(*var, std::env::var(var).ok());
}
EnvGuard {
_mutex_guard: guard,
old_vars,
}
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
for (var, value) in &self.old_vars {
if let Some(val) = value {
std::env::set_var(var, val);
} else {
std::env::remove_var(var);
}
}
}
}
#[test]
fn test_supports_kitty_heuristics() {
let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
std::env::set_var("TERM", "xterm-kitty");
std::env::remove_var("TERMINAL_EMULATOR");
std::env::remove_var("TERM_PROGRAM");
assert!(supports_kitty());
std::env::remove_var("TERM");
std::env::set_var("TERMINAL_EMULATOR", "iterm-kitty");
assert!(supports_kitty());
std::env::set_var("TERMINAL_EMULATOR", "iTerm.app");
assert!(supports_kitty());
std::env::remove_var("TERMINAL_EMULATOR");
std::env::set_var("TERM_PROGRAM", "rio");
assert!(supports_kitty());
std::env::remove_var("TERM_PROGRAM");
assert!(!supports_kitty());
}
#[test]
fn test_supports_iterm2_heuristics() {
let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM"]);
std::env::remove_var("TERM");
std::env::set_var("TERM_PROGRAM", "iTerm.app");
assert!(supports_iterm2());
std::env::set_var("TERM_PROGRAM", "WezTerm");
assert!(supports_iterm2());
std::env::set_var("TERM_PROGRAM", "rio");
assert!(supports_iterm2());
std::env::set_var("TERM_PROGRAM", "Apple_Terminal");
assert!(!supports_iterm2());
std::env::remove_var("TERM_PROGRAM");
assert!(!supports_iterm2());
}
#[test]
fn test_supports_sixel_heuristics() {
let _guard = EnvGuard::new(&["TERM", "TERM_PROGRAM", "WT_SESSION"]);
std::env::remove_var("TERM");
std::env::remove_var("TERM_PROGRAM");
std::env::remove_var("WT_SESSION");
assert!(!supports_sixel());
std::env::set_var("TERM", "xterm-sixel");
assert!(supports_sixel());
std::env::set_var("TERM", "foot");
assert!(supports_sixel());
std::env::set_var("TERM", "MLTerm");
assert!(supports_sixel());
std::env::remove_var("TERM");
std::env::set_var("TERM_PROGRAM", "WezTerm");
assert!(supports_sixel());
std::env::set_var("TERM_PROGRAM", "iTerm.app");
assert!(supports_sixel());
std::env::set_var("TERM_PROGRAM", "rio");
assert!(supports_sixel());
std::env::remove_var("TERM_PROGRAM");
std::env::set_var("WT_SESSION", "active");
assert!(supports_sixel());
}
#[test]
fn test_get_embedded_logo() {
let logo = get_embedded_logo(Some("arch"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("pop"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("manjaro"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("endeavouros"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("opensuse"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("opensuse-leap"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("opensuse-tumbleweed"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("mx"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("linuxmint"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("kali"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("zorin"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("garuda"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("macos"));
assert!(logo.is_some());
let logo = get_embedded_logo(Some("windows"));
assert!(logo.is_some());
let logo = get_embedded_logo(None);
assert!(logo.is_some());
}
#[test]
fn test_get_ascii_logo_new_distros() {
let pop = get_ascii_logo(Some("pop"));
assert!(!pop.is_empty());
assert!(pop.iter().any(|line| line.contains("767")));
let manjaro = get_ascii_logo(Some("manjaro"));
assert!(!manjaro.is_empty());
assert!(manjaro.iter().any(|line| line.contains("████████")));
let endeavouros = get_ascii_logo(Some("endeavouros"));
assert!(!endeavouros.is_empty());
assert!(endeavouros.iter().any(|line| line.contains("ssso")));
let opensuse = get_ascii_logo(Some("opensuse"));
assert!(!opensuse.is_empty());
assert!(opensuse.iter().any(|line| line.contains("O0000Ok")));
let macos = get_ascii_logo(Some("macos"));
assert!(!macos.is_empty());
assert!(macos
.iter()
.any(|line| line.contains("cKMMMMMMMMMMNWMMMMMMMMMM0")));
let windows = get_ascii_logo(Some("windows"));
assert!(!windows.is_empty());
assert!(windows
.iter()
.any(|line| line.contains("AEEEtttt::::ztF") || line.contains("tt:::tt333EE3")));
let mx = get_ascii_logo(Some("mx"));
assert!(!mx.is_empty());
assert!(mx
.iter()
.any(|line| line.contains("MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNMMMMMMMMM")));
let linuxmint = get_ascii_logo(Some("linuxmint"));
assert!(!linuxmint.is_empty());
assert!(linuxmint.iter().any(|line| line.contains("oOOOOOOOOOOo")));
let kali = get_ascii_logo(Some("kali"));
assert!(!kali.is_empty());
assert!(kali.iter().any(|line| line.contains(":ccc")));
let zorin = get_ascii_logo(Some("zorin"));
assert!(!zorin.is_empty());
assert!(zorin
.iter()
.any(|line| line.contains("osssssssssssssssssssso")));
let garuda = get_ascii_logo(Some("garuda"));
assert!(!garuda.is_empty());
assert!(garuda.iter().any(|line| line.contains("888:8898898")));
}
#[test]
fn test_rio_detected_from_term_when_term_program_is_absent() {
let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
std::env::remove_var("TERM_PROGRAM");
std::env::remove_var("TERMINAL_EMULATOR");
std::env::set_var("TERM", "xterm-rio");
assert!(is_rio_terminal());
assert!(supports_kitty());
assert!(supports_iterm2());
assert!(supports_sixel());
}
#[test]
fn test_rio_still_detected_from_term_program() {
let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
std::env::remove_var("TERMINAL_EMULATOR");
std::env::set_var("TERM", "xterm-256color");
std::env::set_var("TERM_PROGRAM", "rio");
assert!(is_rio_terminal());
assert!(supports_kitty());
}
#[test]
fn test_non_rio_term_is_not_matched() {
let _guard = EnvGuard::new(&["TERM", "TERMINAL_EMULATOR", "TERM_PROGRAM"]);
std::env::remove_var("TERM_PROGRAM");
std::env::remove_var("TERMINAL_EMULATOR");
for term in ["xterm-256color", "screen", "linux", "rioja"] {
std::env::set_var("TERM", term);
assert!(!is_rio_terminal(), "{term} should not be detected as Rio");
}
}
#[test]
fn test_fit_logo_cells_preserves_aspect_for_wide_image() {
let fit = fit_logo_cells(384, 108, 10, 20, 45, 10);
assert_eq!(fit.cols, 45);
assert_eq!(fit.rows, 7);
assert!(fit.width_limited);
assert_eq!(kitty_placement_spec(fit), "c=45");
}
#[test]
fn test_fit_logo_cells_preserves_aspect_for_tall_image() {
let fit = fit_logo_cells(291, 384, 10, 20, 45, 10);
assert_eq!(fit.rows, 10);
assert!(
fit.cols < 45,
"tall image should not fill the width, got {}",
fit.cols
);
assert!(!fit.width_limited);
assert_eq!(kitty_placement_spec(fit), "r=10");
}
#[test]
fn test_fit_logo_cells_accounts_for_non_square_cells() {
let fit = fit_logo_cells(256, 256, 10, 20, 45, 10);
assert_eq!((fit.cols, fit.rows), (20, 10));
let sq = fit_logo_cells(256, 256, 10, 10, 45, 10);
assert_eq!((sq.cols, sq.rows), (10, 10));
}
#[test]
fn test_fit_logo_cells_never_exceeds_the_box() {
for (w, h) in [(384, 108), (291, 384), (256, 256), (4000, 3), (3, 4000)] {
let fit = fit_logo_cells(w, h, 10, 20, 45, 10);
assert!((1..=45).contains(&fit.cols), "{w}x{h} -> {} cols", fit.cols);
assert!((1..=10).contains(&fit.rows), "{w}x{h} -> {} rows", fit.rows);
}
}
#[test]
fn test_fit_logo_cells_handles_degenerate_input() {
for fit in [
fit_logo_cells(0, 0, 10, 20, 45, 10),
fit_logo_cells(384, 108, 0, 20, 45, 10),
fit_logo_cells(384, 108, 10, 0, 45, 10),
] {
assert_eq!((fit.cols, fit.rows), (45, 10));
}
}
#[test]
fn test_chafa_size_arg_matches_the_shared_box() {
assert_eq!(chafa_size_arg(), format!("{LOGO_MAX_COLS}x{LOGO_MAX_ROWS}"));
assert_eq!(chafa_size_arg(), "45x10");
}
}