From 517df2bb0006b16d24f8b9b25020a793354ab294 Mon Sep 17 00:00:00 2001 From: xxanqw Date: Sat, 29 Nov 2025 23:42:12 +0200 Subject: Enable ANSI support on Windows and fallback to crossterm colors if necessary --- Cargo.lock | 3 +- Cargo.toml | 6 +++- src/color.rs | 108 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- src/main.rs | 67 +++++++++++++++++++++++++++--------- 4 files changed, 165 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index beaaa6b..7b96e0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -252,12 +252,13 @@ checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "mikusays" -version = "0.1.4" +version = "0.1.4-2" dependencies = [ "clap", "crossterm", "rand", "unicode-width", + "winapi", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e61489c..e7bdafc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mikusays" -version = "0.1.4" +version = "0.1.4-2" edition = "2024" description = "A \"cowsay\" clone with Hatsune Miku ASCII art and speech bubbles." license = "MIT" @@ -31,3 +31,7 @@ crossterm = "0.29" unicode-width = "0.2.2" rand = "0.9.2" clap = { version = "4.5.53", features = ["derive"] } +# sysinfo removed; using env var heuristics instead + +# Windows console API to enable virtual terminal processing (ANSI) +winapi = { version = "0.3", features = ["consoleapi", "wincon", "processenv", "handleapi", "winbase"] } diff --git a/src/color.rs b/src/color.rs index 1b1b201..20fbb38 100644 --- a/src/color.rs +++ b/src/color.rs @@ -1,4 +1,5 @@ use clap::ValueEnum; +// No external sysinfo dependency — use environment heuristics instead for Windows shell detection. use std::error::Error; use std::fmt; use std::str::FromStr; @@ -478,10 +479,115 @@ pub fn detect_color_mode() -> ColorMode { } } - // Default fallback + // If running on Windows, try to enable ANSI (virtual terminal processing) + #[cfg(windows)] + { + if enable_windows_ansi() { + // After enabling ANSI, reuse heuristic + if let Ok(colorterm) = std::env::var("COLORTERM") + && (colorterm.contains("truecolor") || colorterm.contains("24bit")) + { + return ColorMode::Truecolor; + } + + if let Ok(term) = std::env::var("TERM") { + if term.contains("256color") { + return ColorMode::Ansi256; + } + if term.contains("color") { + return ColorMode::Ansi16; + } + } + + return ColorMode::Ansi16; + } else { + // If we couldn't enable ANSI, detect shell and adjust gracefully + if let Some(shell) = detect_windows_shell() { + let shell = shell.to_lowercase(); + if shell.contains("cmd.exe") { + // cmd historically doesn't support ANSI; fallback to Ansi16 + return ColorMode::Ansi16; + } + if shell.contains("powershell") || shell.contains("pwsh") { + // Try to detect term capability + if let Ok(term) = std::env::var("TERM") { + if term.contains("256color") { + return ColorMode::Ansi256; + } + } + // Default to Ansi16 when in older hosts + return ColorMode::Ansi16; + } + } + // Generic fallback if no shell detected + return ColorMode::Ansi16; + } + } + + // Default fallback for non-Windows ColorMode::Ansi16 } +/// Try to enable ANSI (virtual terminal processing) on Windows consoles. +/// Returns true if enabling succeeded or isn't necessary, false otherwise. +#[cfg(windows)] +pub fn enable_windows_ansi() -> bool { + use std::ptr; + use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode}; + use winapi::um::handleapi::INVALID_HANDLE_VALUE; + use winapi::um::processenv::GetStdHandle; + use winapi::um::winbase::STD_OUTPUT_HANDLE; + use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + + unsafe { + let stdout = GetStdHandle(STD_OUTPUT_HANDLE); + if stdout == INVALID_HANDLE_VALUE { + return false; + } + + let mut mode: u32 = 0; + if GetConsoleMode(stdout, &mut mode) == 0 { + return false; + } + + let new_mode = mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if SetConsoleMode(stdout, new_mode) == 0 { + return false; + } + } + + true +} + +/// Detect the parent shell name on Windows, using the sysinfo crate. +#[cfg(windows)] +pub fn detect_windows_shell() -> Option { + // Heuristic checks using common env vars + // PowerShell sets PSModulePath; pwsh may also set POWERSHELL_* env vars. + if std::env::var("PSModulePath").is_ok() { + return Some("powershell".to_string()); + } + + if std::env::var("PWshPROFILE").is_ok() || std::env::var("$PWSH_VERSION").is_ok() { + // Some versions of pwsh may expose env vars; use generic pwsh hint + return Some("pwsh".to_string()); + } + + // COMSPEC usually points to cmd.exe on Windows + if let Ok(comspec) = std::env::var("COMSPEC") { + if comspec.to_lowercase().ends_with("cmd.exe") { + return Some("cmd.exe".to_string()); + } + } + + // Windows Terminal sets WT_SESSION, but it's not a shell; just return terminal info + if std::env::var("WT_SESSION").is_ok() { + return Some("windows_terminal".to_string()); + } + + None +} + /// Linear interpolation between two colors in HSV space pub fn lerp_color_hsv(start: &Color, end: &Color, t: f32) -> Color { let (h1, s1, v1) = rgb_to_hsv(start.to_rgb()); diff --git a/src/main.rs b/src/main.rs index 023f7f3..49ac18d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,11 @@ use clap::{Parser, ValueEnum}; use crossterm::terminal::size; +#[cfg(windows)] +use crossterm::style::{SetForegroundColor, ResetColor, Print}; +#[cfg(windows)] +use crossterm::QueueableCommand; +#[cfg(windows)] +use std::io::stdout; use std::error::Error; use std::str::FromStr; use unicode_width::UnicodeWidthStr; @@ -428,22 +434,51 @@ fn draw_miku_says( let mut colored_line = String::new(); colored_line.push_str(&horizontal_padding); - for c in line.chars() { - if c.is_whitespace() { - colored_line.push(c); - } else if color_index < color_config.color_cache.len() { - let color = &color_config.color_cache[color_index]; - let ansi_code = color.to_ansi_fg(color_config.mode); - colored_line.push_str(&ansi_code); - colored_line.push(c); - colored_line.push_str("\x1b[0m"); // Reset - color_index += 1; - } else { - colored_line.push(c); - } - } - - println!("{}", colored_line); + // If on Windows and ANSI wasn't enabled, fallback to crossterm color APIs + #[cfg(windows)] + { + if !color::enable_windows_ansi() { + let mut stdout = stdout(); + stdout.queue(Print(horizontal_padding.clone())).ok(); + + for c in line.chars() { + if c.is_whitespace() { + stdout.queue(Print(c)).ok(); + } else if color_index < color_config.color_cache.len() { + let color = &color_config.color_cache[color_index]; + let ct_color = color::to_crossterm_color(color); + stdout.queue(SetForegroundColor(ct_color)).ok(); + stdout.queue(Print(c)).ok(); + stdout.queue(ResetColor).ok(); + color_index += 1; + } else { + stdout.queue(Print(c)).ok(); + } + } + + stdout.queue(Print("\n")).ok(); + stdout.flush().ok(); + // Skip the ANSI branch below + continue; + } + } + + for c in line.chars() { + if c.is_whitespace() { + colored_line.push(c); + } else if color_index < color_config.color_cache.len() { + let color = &color_config.color_cache[color_index]; + let ansi_code = color.to_ansi_fg(color_config.mode); + colored_line.push_str(&ansi_code); + colored_line.push(c); + colored_line.push_str("\x1b[0m"); // Reset + color_index += 1; + } else { + colored_line.push(c); + } + } + + println!("{}", colored_line); } else if let Some(single_color) = &color_config.single_color { // Apply single solid color let ansi_code = single_color.to_ansi_fg(color_config.mode); -- cgit v1.3-2-g0d8e