diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 28d35f53..ad3947aa 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -49,6 +49,7 @@ pub(crate) mod safety; pub mod seatbelt; pub mod shell; pub mod spawn; +pub mod terminal; pub mod turn_diff_tracker; pub mod user_agent; mod user_notification; diff --git a/codex-rs/core/src/terminal.rs b/codex-rs/core/src/terminal.rs new file mode 100644 index 00000000..02104f8b --- /dev/null +++ b/codex-rs/core/src/terminal.rs @@ -0,0 +1,72 @@ +use std::sync::OnceLock; + +static TERMINAL: OnceLock = OnceLock::new(); + +pub fn user_agent() -> String { + TERMINAL.get_or_init(detect_terminal).to_string() +} + +/// Sanitize a header value to be used in a User-Agent string. +/// +/// This function replaces any characters that are not allowed in a User-Agent string with an underscore. +/// +/// # Arguments +/// +/// * `value` - The value to sanitize. +fn is_valid_header_value_char(c: char) -> bool { + c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.' || c == '/' +} + +fn sanitize_header_value(value: String) -> String { + value.replace(|c| !is_valid_header_value_char(c), "_") +} + +fn detect_terminal() -> String { + sanitize_header_value( + if let Ok(tp) = std::env::var("TERM_PROGRAM") + && !tp.trim().is_empty() + { + let ver = std::env::var("TERM_PROGRAM_VERSION").ok(); + match ver { + Some(v) if !v.trim().is_empty() => format!("{tp}/{v}"), + _ => tp, + } + } else if let Ok(v) = std::env::var("WEZTERM_VERSION") { + if !v.trim().is_empty() { + format!("WezTerm/{v}") + } else { + "WezTerm".to_string() + } + } else if std::env::var("KITTY_WINDOW_ID").is_ok() + || std::env::var("TERM") + .map(|t| t.contains("kitty")) + .unwrap_or(false) + { + "kitty".to_string() + } else if std::env::var("ALACRITTY_SOCKET").is_ok() + || std::env::var("TERM") + .map(|t| t == "alacritty") + .unwrap_or(false) + { + "Alacritty".to_string() + } else if let Ok(v) = std::env::var("KONSOLE_VERSION") { + if !v.trim().is_empty() { + format!("Konsole/{v}") + } else { + "Konsole".to_string() + } + } else if std::env::var("GNOME_TERMINAL_SCREEN").is_ok() { + return "gnome-terminal".to_string(); + } else if let Ok(v) = std::env::var("VTE_VERSION") { + if !v.trim().is_empty() { + format!("VTE/{v}") + } else { + "VTE".to_string() + } + } else if std::env::var("WT_SESSION").is_ok() { + return "WindowsTerminal".to_string(); + } else { + std::env::var("TERM").unwrap_or_else(|_| "unknown".to_string()) + }, + ) +} diff --git a/codex-rs/core/src/user_agent.rs b/codex-rs/core/src/user_agent.rs index ddcfd4b7..a63170ce 100644 --- a/codex-rs/core/src/user_agent.rs +++ b/codex-rs/core/src/user_agent.rs @@ -4,11 +4,12 @@ pub fn get_codex_user_agent(originator: Option<&str>) -> String { let build_version = env!("CARGO_PKG_VERSION"); let os_info = os_info::get(); format!( - "{}/{build_version} ({} {}; {})", + "{}/{build_version} ({} {}; {}) {}", originator.unwrap_or(DEFAULT_ORIGINATOR), os_info.os_type(), os_info.version(), os_info.architecture().unwrap_or("unknown"), + crate::terminal::user_agent() ) } @@ -27,9 +28,10 @@ mod tests { fn test_macos() { use regex_lite::Regex; let user_agent = get_codex_user_agent(None); - let re = - Regex::new(r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\)$") - .unwrap(); + let re = Regex::new( + r"^codex_cli_rs/\d+\.\d+\.\d+ \(Mac OS \d+\.\d+\.\d+; (x86_64|arm64)\) (\S+)$", + ) + .unwrap(); assert!(re.is_match(&user_agent)); } }