use std::io::Write;
use std::sync::OnceLock;
static OSC_ENABLED: OnceLock<bool> = OnceLock::new();
pub fn configure(enabled: bool) {
OSC_ENABLED
.set(enabled)
.expect("OSC_ENABLED already initialized");
}
pub(crate) fn is_enabled() -> bool {
*OSC_ENABLED.get_or_init(|| true)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProgressState {
None,
Normal,
Error,
Indeterminate,
Warning,
}
impl ProgressState {
fn as_code(&self) -> u8 {
match self {
ProgressState::None => 0,
ProgressState::Normal => 1,
ProgressState::Error => 2,
ProgressState::Indeterminate => 3,
ProgressState::Warning => 4,
}
}
}
fn terminal_supports_osc_9_4() -> bool {
static SUPPORTS_OSC_9_4: OnceLock<bool> = OnceLock::new();
*SUPPORTS_OSC_9_4.get_or_init(|| {
if let Ok(term_program) = std::env::var("TERM_PROGRAM") {
match term_program.as_str() {
"ghostty" => return true,
"vscode" => return true,
"iTerm.app" => return true,
"WezTerm" => return false,
"Alacritty" => return false,
_ => {}
}
}
if std::env::var("WT_SESSION").is_ok() {
return true;
}
if std::env::var("VTE_VERSION").is_ok() {
return true;
}
false
})
}
pub(crate) fn set_progress(state: ProgressState, progress: u8) {
let progress = progress.min(100);
let _ = write_progress(state, progress);
}
fn write_progress(state: ProgressState, progress: u8) -> std::io::Result<()> {
if !is_enabled() || !console::user_attended_stderr() {
return Ok(());
}
if !terminal_supports_osc_9_4() {
return Ok(());
}
let mut stderr = std::io::stderr();
write!(stderr, "\x1b]9;4;{};{}\x1b\\", state.as_code(), progress)?;
stderr.flush()
}
pub(crate) fn clear_progress() {
if is_enabled() {
set_progress(ProgressState::None, 0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_progress_state_codes() {
assert_eq!(ProgressState::None.as_code(), 0);
assert_eq!(ProgressState::Normal.as_code(), 1);
assert_eq!(ProgressState::Error.as_code(), 2);
assert_eq!(ProgressState::Indeterminate.as_code(), 3);
assert_eq!(ProgressState::Warning.as_code(), 4);
}
#[test]
fn test_set_progress_doesnt_panic() {
set_progress(ProgressState::Normal, 50);
set_progress(ProgressState::Indeterminate, 0);
clear_progress();
}
#[test]
fn test_progress_clamping() {
set_progress(ProgressState::Normal, 150);
}
#[test]
fn test_progress_state_equality() {
assert_eq!(ProgressState::None, ProgressState::None);
assert_eq!(ProgressState::Normal, ProgressState::Normal);
assert_eq!(ProgressState::Error, ProgressState::Error);
assert_eq!(ProgressState::Indeterminate, ProgressState::Indeterminate);
assert_eq!(ProgressState::Warning, ProgressState::Warning);
assert_ne!(ProgressState::None, ProgressState::Normal);
assert_ne!(ProgressState::Error, ProgressState::Warning);
}
#[test]
fn test_progress_state_clone() {
let state = ProgressState::Normal;
let cloned = state.clone();
assert_eq!(state, cloned);
}
#[test]
fn test_progress_state_debug() {
let debug_str = format!("{:?}", ProgressState::Normal);
assert_eq!(debug_str, "Normal");
let debug_str = format!("{:?}", ProgressState::Error);
assert_eq!(debug_str, "Error");
}
#[test]
fn test_progress_boundary_values() {
set_progress(ProgressState::Normal, 0);
set_progress(ProgressState::Normal, 100);
set_progress(ProgressState::Normal, 255); }
#[test]
fn test_all_progress_states() {
for state in [
ProgressState::None,
ProgressState::Normal,
ProgressState::Error,
ProgressState::Indeterminate,
ProgressState::Warning,
] {
set_progress(state, 50);
}
}
#[test]
fn test_clear_progress_idempotent() {
clear_progress();
clear_progress();
clear_progress();
}
}