Enable ANSI support on Windows and fallback to crossterm colors if necessary

This commit is contained in:
xxanqw committed 2025-11-29 23:42:12 +02:00
1 parent d2c07879d6
commit 517df2bb00
4 files changed
+164 -18

No files matched your search

Generated
+2 -1
View File
@@ -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]]
+5 -1
View File
@@ -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"] }
+107 -1
View File
@@ -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<String> {
// 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());
+50 -15
View File
@@ -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);
}
}
// 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();
println!("{}", colored_line);
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);