use std::collections::HashMap;
use std::env;
use crate::color::ColorSystem;
use crate::color_env::{detect_color_env, ColorEnvOverride};
use crate::console::Console;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TerminalInfo {
pub term: String,
pub width: usize,
pub height: usize,
pub emulator: Option<String>,
pub is_terminal: bool,
}
impl TerminalInfo {
pub fn detect() -> Self {
let term = env::var("TERM").unwrap_or_default();
let (width, height) = Console::detect_terminal_size();
let emulator = detect_terminal_emulator();
let is_terminal = is_terminal();
Self {
term,
width,
height,
emulator,
is_terminal,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColorSupport {
NoColor,
Standard,
Two56,
TrueColor,
}
impl ColorSupport {
pub fn detect() -> Self {
match detect_color_env() {
ColorEnvOverride::NoColor => ColorSupport::NoColor,
ColorEnvOverride::ForceColor => ColorSupport::Two56,
ColorEnvOverride::ForceColorTruecolor => ColorSupport::TrueColor,
ColorEnvOverride::None => {
if let Ok(colorterm) = env::var("COLORTERM") {
let ct = colorterm.to_lowercase();
if ct.contains("truecolor") || ct.contains("24bit") {
return ColorSupport::TrueColor;
}
}
if let Ok(term) = env::var("TERM") {
let term_lower = term.to_lowercase();
if term_lower.contains("256color") || term_lower.contains("256-color") {
return ColorSupport::Two56;
}
if term_lower.contains("truecolor") || term_lower.contains("24bit") {
return ColorSupport::TrueColor;
}
}
ColorSupport::TrueColor
}
}
}
pub fn name(&self) -> &'static str {
match self {
ColorSupport::NoColor => "No Color",
ColorSupport::Standard => "Standard (16 colors)",
ColorSupport::Two56 => "256 colors",
ColorSupport::TrueColor => "TrueColor (24-bit)",
}
}
pub fn color_system(&self) -> Option<ColorSystem> {
match self {
ColorSupport::NoColor => None,
ColorSupport::Standard => Some(ColorSystem::Standard),
ColorSupport::Two56 => Some(ColorSystem::EightBit),
ColorSupport::TrueColor => Some(ColorSystem::TrueColor),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnicodeSupport {
pub utf8_supported: bool,
pub version: String,
pub emoji_supported: bool,
pub box_drawing_supported: bool,
pub wide_character_supported: bool,
}
impl UnicodeSupport {
pub fn detect() -> Self {
let utf8_supported = detect_utf8_support();
let version = detect_unicode_version();
let emoji_supported = detect_emoji_support();
let box_drawing_supported = detect_box_drawing_support();
let wide_character_supported = true;
Self {
utf8_supported,
version,
emoji_supported,
box_drawing_supported,
wide_character_supported,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlatformInfo {
pub os: String,
pub arch: String,
pub rust_version: String,
pub gilt_version: &'static str,
}
impl PlatformInfo {
pub fn detect() -> Self {
let os = env::consts::OS.to_string();
let arch = env::consts::ARCH.to_string();
let rust_version = rustc_version();
let gilt_version = env!("CARGO_PKG_VERSION");
Self {
os,
arch,
rust_version,
gilt_version,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DiagnosticReport {
pub terminal: TerminalInfo,
pub environment: HashMap<String, String>,
pub color_support: ColorSupport,
pub unicode_support: UnicodeSupport,
pub platform: PlatformInfo,
}
impl DiagnosticReport {
pub fn generate() -> Self {
Self {
terminal: TerminalInfo::detect(),
environment: collect_environment(),
color_support: ColorSupport::detect(),
unicode_support: UnicodeSupport::detect(),
platform: PlatformInfo::detect(),
}
}
}
fn detect_terminal_emulator() -> Option<String> {
if let Ok(program) = env::var("TERM_PROGRAM") {
if !program.is_empty() {
let version = env::var("TERM_PROGRAM_VERSION").unwrap_or_default();
if !version.is_empty() {
return Some(format!("{} {}", program, version));
}
return Some(program);
}
}
if env::var("VTE_VERSION").is_ok() {
return Some("VTE".to_string());
}
if env::var("GNOME_TERMINAL_SERVICE").is_ok() || env::var("GNOME_TERMINAL_SCREEN").is_ok() {
return Some("GNOME Terminal".to_string());
}
if env::var("KONSOLE_VERSION").is_ok() || env::var("KONSOLE_DBUS_SERVICE").is_ok() {
return Some("Konsole".to_string());
}
if env::var("ITERM_SESSION_ID").is_ok() {
return Some("iTerm2".to_string());
}
if env::var("WT_SESSION").is_ok() || env::var("WT_PROFILE_ID").is_ok() {
return Some("Windows Terminal".to_string());
}
if env::var("ALACRITTY_WINDOW_ID").is_ok() || env::var("ALACRITTY_SOCKET").is_ok() {
return Some("Alacritty".to_string());
}
if env::var("WEZTERM_UNIX_SOCKET").is_ok() || env::var("WEZTERM_PANE").is_ok() {
return Some("WezTerm".to_string());
}
if env::var("HYPER").is_ok() {
return Some("Hyper".to_string());
}
if env::var("TMUX").is_ok() || env::var("TMUX_PANE").is_ok() {
return Some("tmux".to_string());
}
if env::var("SCREEN").is_ok() || env::var("STY").is_ok() {
return Some("GNU Screen".to_string());
}
if let Ok(term) = env::var("TERM") {
let term_lower = term.to_lowercase();
if term_lower.contains("xterm") {
return Some("xterm-compatible".to_string());
}
if term_lower.contains("vt100") || term_lower.contains("vt220") {
return Some("VT100/220".to_string());
}
}
None
}
fn is_terminal() -> bool {
use std::io::IsTerminal;
std::io::stdout().is_terminal()
}
fn rustc_version() -> String {
option_env!("RUSTC_VERSION")
.map(|s| s.to_string())
.unwrap_or_else(|| {
format!("{}.{}.{}", env!("CARGO_PKG_RUST_VERSION"), "x", "x")
})
}
fn detect_utf8_support() -> bool {
for var in ["LANG", "LC_ALL", "LC_CTYPE"] {
if let Ok(val) = env::var(var) {
let val_lower = val.to_lowercase();
if val_lower.contains("utf-8") || val_lower.contains("utf8") {
return true;
}
}
}
true
}
fn detect_unicode_version() -> String {
if let Ok(term) = env::var("TERM") {
let term_lower = term.to_lowercase();
if term_lower.contains("emoji") || term_lower.contains("modern") {
return "14.0+ (Emoji 14.0)".to_string();
}
}
"13.0+".to_string()
}
fn detect_emoji_support() -> bool {
if let Ok(term) = env::var("TERM") {
if term.contains("dumb") || term.contains("linux") {
return false;
}
}
true
}
fn detect_box_drawing_support() -> bool {
if let Ok(term) = env::var("TERM") {
let term_lower = term.to_lowercase();
if term_lower.contains("dumb") || term_lower.contains("vt100") {
return false;
}
}
true
}
fn collect_environment() -> HashMap<String, String> {
let vars = [
"NO_COLOR",
"FORCE_COLOR",
"CLICOLOR",
"CLICOLOR_FORCE",
"COLORTERM",
"TERM_PROGRAM",
"TERM_PROGRAM_VERSION",
"COLUMNS",
"LINES",
"REDUCE_MOTION",
"LANG",
"LC_ALL",
"LC_CTYPE",
"TMUX",
"SCREEN",
"VTE_VERSION",
"ITERM_SESSION_ID",
"WT_SESSION",
"KONSOLE_VERSION",
"ALACRITTY_WINDOW_ID",
"WEZTERM_PANE",
];
let mut result = HashMap::new();
for var in &vars {
if let Ok(val) = env::var(var) {
if !val.is_empty() {
result.insert(var.to_string(), val);
}
}
}
result
}
pub fn report() -> String {
let report = DiagnosticReport::generate();
format_report(&report)
}
pub fn print_report() {
println!("{}", report());
}
fn format_report(report: &DiagnosticReport) -> String {
let mut output = String::new();
output.push_str("╔══════════════════════════════════════════════════════════════╗\n");
output.push_str("║ gilt Terminal Diagnostic Report ║\n");
output.push_str("╚══════════════════════════════════════════════════════════════╝\n\n");
output.push_str("━ Platform Information ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
output.push_str(&format!(" OS: {}\n", report.platform.os));
output.push_str(&format!(" Architecture: {}\n", report.platform.arch));
output.push_str(&format!(
" Rust Version: {}\n",
report.platform.rust_version
));
output.push_str(&format!(
" gilt Version: {}\n",
report.platform.gilt_version
));
output.push('\n');
output.push_str("━ Terminal Information ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
output.push_str(&format!(" Terminal Type: {}\n", report.terminal.term));
output.push_str(&format!(
" Dimensions: {} columns × {} rows\n",
report.terminal.width, report.terminal.height
));
output.push_str(&format!(
" Interactive: {}\n",
if report.terminal.is_terminal {
"Yes"
} else {
"No"
}
));
if let Some(ref emulator) = report.terminal.emulator {
output.push_str(&format!(" Emulator: {}\n", emulator));
}
output.push('\n');
output.push_str("━ Color Support ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
output.push_str(&format!(
" Level: {}\n",
report.color_support.name()
));
output.push_str(&format!(
" Standard (16): {}\n",
if report.color_support as u8 >= ColorSupport::Standard as u8 {
"✓ Yes"
} else {
"✗ No"
}
));
output.push_str(&format!(
" 256 colors: {}\n",
if report.color_support as u8 >= ColorSupport::Two56 as u8 {
"✓ Yes"
} else {
"✗ No"
}
));
output.push_str(&format!(
" TrueColor: {}\n",
if report.color_support == ColorSupport::TrueColor {
"✓ Yes"
} else {
"✗ No"
}
));
output.push('\n');
output.push_str("━ Unicode Support ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
output.push_str(&format!(
" UTF-8: {}\n",
if report.unicode_support.utf8_supported {
"✓ Yes"
} else {
"✗ No"
}
));
output.push_str(&format!(
" Version: {}\n",
report.unicode_support.version
));
output.push_str(&format!(
" Emoji: {}\n",
if report.unicode_support.emoji_supported {
"✓ Yes"
} else {
"✗ No"
}
));
output.push_str(&format!(
" Box Drawing: {}\n",
if report.unicode_support.box_drawing_supported {
"✓ Yes"
} else {
"✗ No"
}
));
output.push_str(&format!(
" Wide Characters: {}\n",
if report.unicode_support.wide_character_supported {
"✓ Yes"
} else {
"✗ No"
}
));
output.push('\n');
output.push_str("━ Environment Variables ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
output.push_str(" Color-related:\n");
for var in [
"NO_COLOR",
"FORCE_COLOR",
"CLICOLOR",
"CLICOLOR_FORCE",
"COLORTERM",
] {
if let Ok(val) = env::var(var) {
output.push_str(&format!(" {} = \"{}\"\n", var, val));
} else {
output.push_str(&format!(" {} (not set)\n", var));
}
}
output.push_str("\n Size-related:\n");
for var in ["COLUMNS", "LINES"] {
if let Ok(val) = env::var(var) {
output.push_str(&format!(" {} = \"{}\"\n", var, val));
} else {
output.push_str(&format!(" {} (not set)\n", var));
}
}
output.push_str("\n Other:\n");
if let Ok(val) = env::var("REDUCE_MOTION") {
output.push_str(&format!(" REDUCE_MOTION = \"{}\"\n", val));
} else {
output.push_str(" REDUCE_MOTION (not set)\n");
}
if let Ok(val) = env::var("TERM_PROGRAM") {
output.push_str(&format!(" TERM_PROGRAM = \"{}\"\n", val));
if let Ok(ver) = env::var("TERM_PROGRAM_VERSION") {
output.push_str(&format!(" TERM_PROGRAM_VERSION = \"{}\"\n", ver));
}
}
output.push('\n');
output.push_str("━ Color Test ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
output.push_str(" Standard colors: ");
for i in 0..8 {
output.push_str(&format!("\x1b[3{}m█\x1b[0m", i));
}
output.push('\n');
output.push_str(" Bright colors: ");
for i in 0..8 {
output.push_str(&format!("\x1b[9{}m█\x1b[0m", i));
}
output.push('\n');
if report.color_support as u8 >= ColorSupport::Two56 as u8 {
output.push_str(" 256 colors: ");
for i in [0, 36, 72, 108, 144, 180, 216] {
output.push_str(&format!("\x1b[38;5;{}m█\x1b[0m", i));
}
output.push('\n');
}
if report.color_support == ColorSupport::TrueColor {
output.push_str(" TrueColor: ");
for i in 0..8 {
let r = (i * 36) as u8;
let g = ((7 - i) * 36) as u8;
output.push_str(&format!("\x1b[38;2;{};{};0m█\x1b[0m", r, g));
}
output.push('\n');
}
output.push('\n');
output.push_str("━ Unicode Test ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
output.push_str(" Box drawing: ─│┌┐└┘├┤┬┴┼\n");
output.push_str(" Rounded: ─│╭╮╰╯├┤┬┴┼\n");
output.push_str(" Heavy: ━┃┏┓┗┛┣┫┳┻╋\n");
output.push_str(" Double: ═║╔╗╚╝╠╣╦╩╬\n");
output.push_str(" Emoji: 🎨 💡 🔧 📊 ✅ ❌\n");
output.push_str(" Arrows: ← ↑ → ↓ ↔ ↔ ↕\n");
output.push_str(" Symbols: ■ □ ● ○ ◆ ◇ ★ ☆\n");
output.push('\n');
output
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_support_detection() {
let _support = ColorSupport::detect();
}
#[test]
fn test_terminal_info_detection() {
let info = TerminalInfo::detect();
assert!(info.width > 0);
assert!(info.height > 0);
}
#[test]
fn test_platform_info_detection() {
let info = PlatformInfo::detect();
assert!(!info.os.is_empty());
assert!(!info.arch.is_empty());
assert!(!info.rust_version.is_empty());
assert!(!info.gilt_version.is_empty());
}
#[test]
fn test_unicode_support_detection() {
let support = UnicodeSupport::detect();
assert!(support.utf8_supported);
}
#[test]
fn test_diagnostic_report_generation() {
let report = DiagnosticReport::generate();
assert!(!report.platform.os.is_empty());
assert!(report.terminal.width > 0);
}
#[test]
fn test_report_formatting() {
let report = DiagnosticReport::generate();
let formatted = format_report(&report);
assert!(formatted.contains("gilt Terminal Diagnostic Report"));
assert!(formatted.contains(&report.platform.os));
assert!(formatted.contains(report.platform.gilt_version));
}
#[test]
fn test_color_support_name() {
assert_eq!(ColorSupport::NoColor.name(), "No Color");
assert_eq!(ColorSupport::Standard.name(), "Standard (16 colors)");
assert_eq!(ColorSupport::Two56.name(), "256 colors");
assert_eq!(ColorSupport::TrueColor.name(), "TrueColor (24-bit)");
}
#[test]
fn test_color_support_color_system() {
assert!(ColorSupport::NoColor.color_system().is_none());
assert_eq!(
ColorSupport::Standard.color_system(),
Some(ColorSystem::Standard)
);
assert_eq!(
ColorSupport::Two56.color_system(),
Some(ColorSystem::EightBit)
);
assert_eq!(
ColorSupport::TrueColor.color_system(),
Some(ColorSystem::TrueColor)
);
}
}