use crate::context::tsk_env::TskEnv;
use is_terminal::IsTerminal;
use std::io::{self, Write};
use std::sync::Mutex;
pub struct TerminalOperations {
state: Mutex<TerminalState>,
}
struct TerminalState {
supported: bool,
original_title: Option<String>,
}
impl TerminalOperations {
pub fn new() -> Self {
let supported = Self::is_supported(None);
Self {
state: Mutex::new(TerminalState {
supported,
original_title: None,
}),
}
}
pub(crate) fn is_supported(tsk_env: Option<&TskEnv>) -> bool {
if !std::io::stdout().is_terminal() {
return false;
}
let term = if let Some(env) = tsk_env {
env.terminal_type().map(|s| s.to_string())
} else {
std::env::var("TERM").ok()
};
if let Some(term_type) = term {
!matches!(term_type.as_str(), "dumb" | "unknown")
} else {
false
}
}
fn write_title(title: &str) {
let _ = write!(io::stdout(), "\x1b]0;{title}\x07");
let _ = io::stdout().flush();
}
pub fn set_title(&self, title: &str) {
let mut state = self.state.lock().unwrap();
if !state.supported {
return;
}
if state.original_title.is_none() {
state.original_title = Some("Terminal".to_string());
}
Self::write_title(title);
}
pub fn restore_title(&self) {
let state = self.state.lock().unwrap();
if !state.supported {
return;
}
if let Some(ref original) = state.original_title {
Self::write_title(original);
}
}
}
impl Drop for TerminalOperations {
fn drop(&mut self) {
self.restore_title();
}
}
impl Default for TerminalOperations {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_terminal_operations_creation() {
let terminal_ops = TerminalOperations::new();
let state = terminal_ops.state.lock().unwrap();
assert!(state.original_title.is_none());
}
#[test]
fn test_set_title_no_panic() {
let terminal_ops = TerminalOperations::new();
terminal_ops.set_title("Test Title");
terminal_ops.restore_title();
}
#[test]
fn test_drop_restores_title() {
{
let terminal_ops = TerminalOperations::new();
terminal_ops.set_title("Temporary Title");
}
}
#[test]
fn test_terminal_support_detection() {
let env_xterm = TskEnv::builder()
.with_terminal_type(Some("xterm-256color".to_string()))
.build()
.unwrap();
assert!(
TerminalOperations::is_supported(Some(&env_xterm)) || !std::io::stdout().is_terminal()
);
let env_dumb = TskEnv::builder()
.with_terminal_type(Some("dumb".to_string()))
.build()
.unwrap();
assert!(!TerminalOperations::is_supported(Some(&env_dumb)));
let env_none = TskEnv::builder().with_terminal_type(None).build().unwrap();
assert!(!TerminalOperations::is_supported(Some(&env_none)));
}
#[test]
fn test_concurrent_access() {
use std::sync::Arc;
use std::thread;
let terminal_ops = Arc::new(TerminalOperations::new());
let mut handles = vec![];
for i in 0..5 {
let ops = Arc::clone(&terminal_ops);
let handle = thread::spawn(move || {
ops.set_title(&format!("Thread {i}"));
ops.restore_title();
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
}
}