Enable ANSI support on Windows and fallback to crossterm colors if necessary
This commit is contained in:
1 parent
d2c07879d6
commit
517df2bb00
4 files changed
+164
-18
No files matched your search
Generated
+2
-1
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user