1use std::io::{self, Write};
2use std::sync::Mutex;
3
4pub trait TerminalOperations: Send + Sync {
6 fn set_title(&self, title: &str);
8
9 fn restore_title(&self);
11}
12
13pub struct DefaultTerminalOperations {
15 state: Mutex<TerminalState>,
16}
17
18struct TerminalState {
19 supported: bool,
20 original_title: Option<String>,
21}
22
23impl DefaultTerminalOperations {
24 pub fn new() -> Self {
26 let supported = Self::is_supported();
27
28 Self {
29 state: Mutex::new(TerminalState {
30 supported,
31 original_title: None,
32 }),
33 }
34 }
35
36 fn is_supported() -> bool {
38 if !atty::is(atty::Stream::Stdout) {
40 return false;
41 }
42
43 if let Ok(term) = std::env::var("TERM") {
45 !matches!(term.as_str(), "dumb" | "unknown")
48 } else {
49 false
50 }
51 }
52
53 fn write_title(title: &str) {
55 let _ = write!(io::stdout(), "\x1b]0;{title}\x07");
59 let _ = io::stdout().flush();
60 }
61}
62
63impl TerminalOperations for DefaultTerminalOperations {
64 fn set_title(&self, title: &str) {
65 let mut state = self.state.lock().unwrap();
66
67 if !state.supported {
68 return;
69 }
70
71 if state.original_title.is_none() {
73 state.original_title = Some("Terminal".to_string());
76 }
77
78 Self::write_title(title);
79 }
80
81 fn restore_title(&self) {
82 let state = self.state.lock().unwrap();
83
84 if !state.supported {
85 return;
86 }
87
88 if let Some(ref original) = state.original_title {
89 Self::write_title(original);
90 }
91 }
92}
93
94impl Drop for DefaultTerminalOperations {
95 fn drop(&mut self) {
96 self.restore_title();
98 }
99}
100
101impl Default for DefaultTerminalOperations {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110
111 #[test]
112 fn test_terminal_operations_creation() {
113 let terminal_ops = DefaultTerminalOperations::new();
114 let state = terminal_ops.state.lock().unwrap();
117 assert!(state.original_title.is_none());
118 }
119
120 #[test]
121 fn test_set_title_no_panic() {
122 let terminal_ops = DefaultTerminalOperations::new();
123 terminal_ops.set_title("Test Title");
125 terminal_ops.restore_title();
126 }
127
128 #[test]
129 fn test_drop_restores_title() {
130 {
131 let terminal_ops = DefaultTerminalOperations::new();
132 terminal_ops.set_title("Temporary Title");
133 }
135 }
137
138 #[test]
139 fn test_terminal_support_detection() {
140 unsafe {
142 std::env::set_var("TERM", "xterm-256color");
143 }
144 assert!(DefaultTerminalOperations::is_supported() || !atty::is(atty::Stream::Stdout));
145
146 unsafe {
147 std::env::set_var("TERM", "dumb");
148 }
149 assert!(!DefaultTerminalOperations::is_supported());
150
151 unsafe {
152 std::env::remove_var("TERM");
153 }
154 assert!(!DefaultTerminalOperations::is_supported());
155 }
156
157 #[test]
158 fn test_concurrent_access() {
159 use std::sync::Arc;
160 use std::thread;
161
162 let terminal_ops = Arc::new(DefaultTerminalOperations::new());
163 let mut handles = vec![];
164
165 for i in 0..5 {
166 let ops = Arc::clone(&terminal_ops);
167 let handle = thread::spawn(move || {
168 ops.set_title(&format!("Thread {i}"));
169 ops.restore_title();
170 });
171 handles.push(handle);
172 }
173
174 for handle in handles {
175 handle.join().unwrap();
176 }
177 }
178}