Refactor color detection and ANSI handling for improved platform compatibility
This commit is contained in:
1 parent
915614c799
commit
77d0b7659c
2 files changed
+117
-147
No files matched your search
+85
-91
@@ -458,81 +458,13 @@ fn ansi_to_rgb(code: u8) -> (u8, u8, u8) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the best available color mode for the terminal
|
||||
pub fn detect_color_mode() -> ColorMode {
|
||||
if std::env::var("NO_COLOR").is_ok() {
|
||||
return ColorMode::NoColor;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// (Removed older detect_color_mode to keep a single refactored implementation)
|
||||
|
||||
/// 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;
|
||||
// no ptr usage required; removed unused import
|
||||
use winapi::um::consoleapi::{GetConsoleMode, SetConsoleMode};
|
||||
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
|
||||
use winapi::um::processenv::GetStdHandle;
|
||||
@@ -559,49 +491,95 @@ pub fn enable_windows_ansi() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Detect the parent shell name on Windows, using the sysinfo crate.
|
||||
/// Detect the parent shell name on Windows, using environment hints.
|
||||
#[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
|
||||
if std::env::var("PWshPROFILE").is_ok() || std::env::var("PWSH_VERSION").is_ok() {
|
||||
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());
|
||||
let (h2, s2, v2) = rgb_to_hsv(end.to_rgb());
|
||||
#[cfg(not(windows))]
|
||||
#[allow(dead_code)]
|
||||
pub fn detect_windows_shell() -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
// Handle hue interpolation correctly (shortest path around the color wheel)
|
||||
let h_diff = (h2 - h1 + 180.0) % 360.0 - 180.0;
|
||||
let h = (h1 + h_diff * t + 360.0) % 360.0;
|
||||
/// Convert Color to a crossterm Color (helper for use across platforms)
|
||||
pub fn to_crossterm_color(color: &Color) -> crossterm::style::Color {
|
||||
let (r, g, b) = color.to_rgb();
|
||||
crossterm::style::Color::Rgb { r, g, b }
|
||||
}
|
||||
|
||||
let s = s1 + (s2 - s1) * t;
|
||||
let v = v1 + (v2 - v1) * t;
|
||||
/// Detect the parent shell name on Windows, using the sysinfo crate.
|
||||
pub fn detect_color_mode() -> ColorMode {
|
||||
if std::env::var("NO_COLOR").is_ok() {
|
||||
return ColorMode::NoColor;
|
||||
}
|
||||
|
||||
let (r, g, b) = hsv_to_rgb(h, s, v);
|
||||
Color::Truecolor(r, g, b)
|
||||
// Early checks
|
||||
if std::env::var("COLORTERM").is_ok_and(|colorterm| {
|
||||
colorterm.contains("truecolor") || colorterm.contains("24bit")
|
||||
}) {
|
||||
return ColorMode::Truecolor;
|
||||
}
|
||||
|
||||
if std::env::var("TERM").is_ok_and(|term| term.contains("256color")) {
|
||||
return ColorMode::Ansi256;
|
||||
}
|
||||
if std::env::var("TERM").is_ok_and(|term| term.contains("color")) {
|
||||
return ColorMode::Ansi16;
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
let mode = ColorMode::Ansi16;
|
||||
|
||||
// If running on Windows, try to enable ANSI (virtual terminal processing)
|
||||
#[cfg(windows)]
|
||||
{
|
||||
if enable_windows_ansi() {
|
||||
// Re-check for a better mode
|
||||
if let Ok(colorterm) = std::env::var("COLORTERM") {
|
||||
if colorterm.contains("truecolor") || colorterm.contains("24bit") {
|
||||
mode = ColorMode::Truecolor;
|
||||
}
|
||||
}
|
||||
if let Ok(term) = std::env::var("TERM") {
|
||||
if term.contains("256color") {
|
||||
mode = ColorMode::Ansi256;
|
||||
}
|
||||
}
|
||||
} else if let Some(shell) = detect_windows_shell() {
|
||||
let shell = shell.to_lowercase();
|
||||
if shell.contains("cmd.exe") {
|
||||
// cmd historically doesn't support ANSI; use 16 colors fallback
|
||||
mode = ColorMode::Ansi16;
|
||||
} else if shell.contains("powershell") || shell.contains("pwsh") {
|
||||
if let Ok(term) = std::env::var("TERM") {
|
||||
if term.contains("256color") {
|
||||
mode = ColorMode::Ansi256;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mode = ColorMode::Ansi16;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mode
|
||||
}
|
||||
|
||||
/// Convert RGB to HSV
|
||||
@@ -632,6 +610,22 @@ fn rgb_to_hsv((r, g, b): (u8, u8, u8)) -> (f32, f32, f32) {
|
||||
(h, s, v)
|
||||
}
|
||||
|
||||
/// 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());
|
||||
let (h2, s2, v2) = rgb_to_hsv(end.to_rgb());
|
||||
|
||||
// Handle hue interpolation correctly (shortest path around the color wheel)
|
||||
let h_diff = (h2 - h1 + 180.0) % 360.0 - 180.0;
|
||||
let h = (h1 + h_diff * t + 360.0) % 360.0;
|
||||
|
||||
let s = s1 + (s2 - s1) * t;
|
||||
let v = v1 + (v2 - v1) * t;
|
||||
|
||||
let (r, g, b) = hsv_to_rgb(h, s, v);
|
||||
Color::Truecolor(r, g, b)
|
||||
}
|
||||
|
||||
/// Convert HSV to RGB
|
||||
fn hsv_to_rgb(h: f32, s: f32, v: f32) -> (u8, u8, u8) {
|
||||
let c = v * s;
|
||||
|
||||
+32
-56
@@ -1,12 +1,9 @@
|
||||
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 crossterm::style::{Print, ResetColor, SetForegroundColor};
|
||||
use crossterm::terminal::size;
|
||||
use std::error::Error;
|
||||
use std::io::{Write, stdout};
|
||||
use std::str::FromStr;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
@@ -431,59 +428,38 @@ fn draw_miku_says(
|
||||
|
||||
// Apply coloring to Miku art lines
|
||||
if color_config.mode != ColorMode::NoColor && !color_config.color_cache.is_empty() {
|
||||
let mut colored_line = String::new();
|
||||
colored_line.push_str(&horizontal_padding);
|
||||
// Use crossterm queueing to color characters so it's platform-agnostic
|
||||
let mut stdout = stdout();
|
||||
stdout.queue(Print(horizontal_padding.clone())).ok();
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
stdout.queue(Print("\n")).ok();
|
||||
stdout.flush().ok();
|
||||
continue;
|
||||
} 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);
|
||||
let reset_code = "\x1b[0m";
|
||||
println!("{}{}{}{}", horizontal_padding, ansi_code, line, reset_code);
|
||||
// Apply single solid color using crossterm
|
||||
let mut stdout = stdout();
|
||||
let ct_color = color::to_crossterm_color(single_color);
|
||||
stdout.queue(Print(horizontal_padding.clone())).ok();
|
||||
stdout.queue(SetForegroundColor(ct_color)).ok();
|
||||
stdout.queue(Print(line)).ok();
|
||||
stdout.queue(ResetColor).ok();
|
||||
stdout.queue(Print("\n")).ok();
|
||||
stdout.flush().ok();
|
||||
} else {
|
||||
// No coloring
|
||||
println!("{}{}", horizontal_padding, line);
|
||||
|
||||
Reference in new issue
Block a user