aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorІван Потієнко <[email protected]>2025-11-29 14:40:52 -0800
committerGitHub <[email protected]>2025-11-29 14:40:52 -0800
commitb4a16e57b51ae2b6d15f521c76b4446267d75c5d (patch)
tree3771456e22362f2123c62e5a2c055d2ac9db3850 /src
parentd2c07879d69d4c6cbe9de7df38cd9add2aa2fb5c (diff)
parent511553a4a2edc051f8a9f0826432147aac4b0032 (diff)
downloadmikusays-c-b4a16e57b51ae2b6d15f521c76b4446267d75c5d.tar.gz
mikusays-c-b4a16e57b51ae2b6d15f521c76b4446267d75c5d.zip
Merge pull request #3 from xxanqw:fix-no-colors-on-windows
Enable ANSI support and improve color handling on Windows
Diffstat (limited to 'src')
-rw-r--r--src/color.rs198
-rw-r--r--src/main.rs118
2 files changed, 243 insertions, 73 deletions
diff --git a/src/color.rs b/src/color.rs
index 1b1b201..548c222 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;
@@ -374,47 +375,22 @@ fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> u8 {
}
/// Convert RGB to ANSI 16 color code
-fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> u8 {
- // Calculate brightness
+pub(crate) fn rgb_to_ansi16(r: u8, g: u8, b: u8) -> u8 {
+ // Calculate brightness (perceived luminance)
let brightness = (r as f32 * 0.299 + g as f32 * 0.587 + b as f32 * 0.114).round() as u8;
-
- // Determine if it's bright or dark
let is_bright = brightness > 128;
- // Determine base color
- let base_color = if r > g && r > b {
- // Red dominant
- if r > 192 && g < 64 && b < 64 {
- 1 // Red
- } else if r > g && r > b {
- 3 // Yellow
- } else {
- 7 // White
- }
- } else if g > r && g > b {
- // Green dominant
- if g > 192 && r < 64 && b < 64 {
- 2 // Green
- } else if g > b {
- 3 // Yellow
- } else {
- 6 // Cyan
- }
- } else {
- // Blue dominant
- if b > 192 && r < 64 && g < 64 {
- 4 // Blue
- } else if b > r {
- 6 // Cyan
- } else {
- 5 // Magenta
- }
- };
+ // Determine base color using simple thresholding on each channel, producing an index 0..7
+ let r_on = r > 128;
+ let g_on = g > 128;
+ let b_on = b > 128;
+ let base_idx = (r_on as u8) + (g_on as u8) * 2 + (b_on as u8) * 4; // 0..7
+ // SGR color codes: 30..37 for normal, 90..97 for bright
if is_bright {
- base_color + 8 // Bright colors are 8-15
+ 90 + base_idx
} else {
- base_color // Normal colors are 0-7
+ 30 + base_idx
}
}
@@ -457,45 +433,131 @@ fn ansi_to_rgb(code: u8) -> (u8, u8, u8) {
}
}
-/// Detect the best available color mode for the terminal
+// (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 {
+ // 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;
+ 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 environment hints.
+#[cfg(windows)]
+pub fn detect_windows_shell() -> Option<String> {
+ // Heuristic checks using common 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() {
+ return Some("pwsh".to_string());
+ }
+ if let Ok(comspec) = std::env::var("COMSPEC") {
+ if comspec.to_lowercase().ends_with("cmd.exe") {
+ return Some("cmd.exe".to_string());
+ }
+ }
+ if std::env::var("WT_SESSION").is_ok() {
+ return Some("windows_terminal".to_string());
+ }
+ None
+}
+
+#[cfg(not(windows))]
+#[allow(dead_code)]
+pub fn detect_windows_shell() -> Option<String> {
+ None
+}
+
+/// 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 }
+}
+
+/// 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;
}
- if let Ok(colorterm) = std::env::var("COLORTERM")
- && (colorterm.contains("truecolor") || colorterm.contains("24bit"))
+ // Early checks
+ if std::env::var("COLORTERM")
+ .is_ok_and(|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 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
- ColorMode::Ansi16
-}
-
-/// 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;
+ #[cfg(windows)]
+ let mut mode = ColorMode::Ansi16;
+ #[cfg(not(windows))]
+ let mode = ColorMode::Ansi16;
- let s = s1 + (s2 - s1) * t;
- let v = v1 + (v2 - v1) * t;
+ // 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;
+ }
+ }
+ }
- let (r, g, b) = hsv_to_rgb(h, s, v);
- Color::Truecolor(r, g, b)
+ mode
}
/// Convert RGB to HSV
@@ -526,6 +588,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;
diff --git a/src/main.rs b/src/main.rs
index 023f7f3..8a50a54 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,9 @@
use clap::{Parser, ValueEnum};
+use crossterm::QueueableCommand;
+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;
@@ -55,6 +58,9 @@ pub struct Args {
/// Disable all coloring (respects NO_COLOR environment variable)
#[arg(long)]
pub no_color: bool,
+ /// Print color detection/debug info and exit
+ #[arg(long)]
+ pub color_debug: bool,
}
#[derive(Debug)]
@@ -66,6 +72,8 @@ pub struct ColorConfig {
pub single_color: Option<Color>,
pub gradient: Option<(Color, Color)>,
pub color_cache: Vec<Color>,
+ pub ansi_enabled: bool,
+ pub debug: bool,
}
pub fn parse_color_args(args: &Args) -> Result<ColorConfig, Box<dyn Error>> {
@@ -157,6 +165,12 @@ pub fn parse_color_args(args: &Args) -> Result<ColorConfig, Box<dyn Error>> {
std::process::exit(1);
}
+ // Determine if ANSI is enabled (Windows toggle helper) — don't enable if --no-color
+ #[cfg(windows)]
+ let ansi_enabled = !args.no_color && color::enable_windows_ansi();
+ #[cfg(not(windows))]
+ let ansi_enabled = false;
+
Ok(ColorConfig {
mode: color_mode,
rainbow: args.rainbow,
@@ -165,6 +179,8 @@ pub fn parse_color_args(args: &Args) -> Result<ColorConfig, Box<dyn Error>> {
single_color,
gradient,
color_cache: Vec::new(),
+ ansi_enabled,
+ debug: args.color_debug,
})
}
@@ -425,30 +441,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();
for c in line.chars() {
if c.is_whitespace() {
- colored_line.push(c);
+ stdout.queue(Print(c)).ok();
} 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
+ 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 {
- colored_line.push(c);
+ stdout.queue(Print(c)).ok();
}
}
- 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);
@@ -481,6 +505,38 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse color-related arguments
let mut color_config = parse_color_args(&args)?;
+ if color_config.debug {
+ println!("Detected color mode: {:?}", color_config.mode);
+ println!("ANSI enabled: {}", color_config.ansi_enabled);
+ println!("NO_COLOR: {:?}", std::env::var("NO_COLOR").ok());
+ println!("COLORTERM: {:?}", std::env::var("COLORTERM").ok());
+ println!("TERM: {:?}", std::env::var("TERM").ok());
+ #[cfg(windows)]
+ {
+ println!("Windows shell hint: {:?}", color::detect_windows_shell());
+ }
+ return Ok(());
+ }
+
+ // If color debug is requested, print information and exit
+ if args.color_debug {
+ eprintln!(
+ "Color Debug: mode={:?}, ansi_enabled={}, rainbow={}, saturation={}, brightness={}",
+ color_config.mode,
+ color_config.ansi_enabled,
+ color_config.rainbow,
+ color_config.saturation,
+ color_config.brightness
+ );
+ if let Some(single) = &color_config.single_color {
+ eprintln!("Single color: {:?}", single);
+ }
+ if let Some((start, end)) = &color_config.gradient {
+ eprintln!("Gradient: {:?} -> {:?}", start, end);
+ }
+ return Ok(());
+ }
+
draw_miku_says(&text, style, &mut color_config)?;
Ok(())
@@ -574,6 +630,7 @@ mod tests {
color: None,
gradient: None,
no_color: false,
+ color_debug: false,
};
let config = parse_color_args(&args).unwrap();
@@ -598,6 +655,7 @@ mod tests {
color: None,
gradient: None,
no_color: true,
+ color_debug: false,
};
let config = parse_color_args(&args).unwrap();
@@ -645,6 +703,8 @@ mod tests {
single_color: None,
gradient: None,
color_cache: vec![Color::Hex("#000000".to_string())], // Pre-filled
+ ansi_enabled: false,
+ debug: false,
};
// Test with 0 characters
@@ -666,6 +726,8 @@ mod tests {
single_color: None,
gradient: None,
color_cache: vec![Color::Hex("#000000".to_string())], // Pre-filled
+ ansi_enabled: false,
+ debug: false,
};
let start = Color::Hex("#FF0000".to_string());
@@ -690,6 +752,8 @@ mod tests {
single_color: None,
gradient: None,
color_cache: Vec::new(),
+ ansi_enabled: false,
+ debug: false,
};
generate_rainbow_cache(&mut config, 10);
@@ -702,6 +766,14 @@ mod tests {
}
#[test]
+ fn test_rgb_to_ansi16_mapping() {
+ // Validate mapping for basic colors
+ assert_eq!(crate::color::rgb_to_ansi16(255, 0, 0), 31);
+ assert_eq!(crate::color::rgb_to_ansi16(255, 255, 255), 97);
+ assert_eq!(crate::color::rgb_to_ansi16(0, 0, 0), 30);
+ }
+
+ #[test]
fn test_generate_gradient_cache() {
let mut config = ColorConfig {
mode: ColorMode::Truecolor,
@@ -711,6 +783,8 @@ mod tests {
single_color: None,
gradient: None,
color_cache: Vec::new(),
+ ansi_enabled: false,
+ debug: false,
};
let start = Color::Hex("#FF0000".to_string()); // Red
@@ -725,4 +799,22 @@ mod tests {
let last_color = &config.color_cache[9];
assert_ne!(first_color, last_color);
}
+
+ #[test]
+ fn test_parse_color_debug_flag() {
+ let args = Args {
+ text: Some("test".to_string()),
+ style: None,
+ list: false,
+ rainbow: false,
+ saturation: 100,
+ brightness: 50,
+ color: None,
+ gradient: None,
+ no_color: false,
+ color_debug: true,
+ };
+ let config = parse_color_args(&args).unwrap();
+ assert!(config.debug);
+ }
}