1use crate::tab::Tab;
8use crate::ui_constants::VISUAL_BELL_FLASH_DURATION_MS;
9
10impl Tab {
11 pub fn is_bell_active(&self) -> bool {
13 if let Some(flash_start) = self.active_bell().visual_flash {
15 flash_start.elapsed().as_millis() < VISUAL_BELL_FLASH_DURATION_MS
16 } else {
17 false
18 }
19 }
20
21 pub fn update_title(
33 &mut self,
34 title_mode: par_term_config::TabTitleMode,
35 remote_format: par_term_config::RemoteTabTitleFormat,
36 remote_osc_priority: bool,
37 ) {
38 if self.user_named {
40 return;
41 }
42
43 let focused_id = self
47 .pane_manager
48 .as_ref()
49 .and_then(|pm| pm.focused_pane_id());
50
51 let local_hostname = hostname::get().ok().and_then(|h| h.into_string().ok());
53 let home_dir = dirs::home_dir();
54
55 if let Some(pm) = self.pane_manager.as_mut() {
59 for pane in pm.all_panes_mut() {
60 if let Ok(term) = pane.terminal.try_read() {
61 let osc_title = term.get_title();
62 let hostname = term.shell_integration_hostname();
63 let username = term.shell_integration_username();
64 let cwd = term.shell_integration_cwd();
65 drop(term);
66
67 let is_remote = if let Some(reported_host) = &hostname {
68 local_hostname
69 .as_ref()
70 .map(|local| !reported_host.eq_ignore_ascii_case(local))
71 .unwrap_or(false)
72 } else {
73 false
74 };
75
76 if is_remote {
77 if remote_osc_priority && !osc_title.is_empty() {
78 pane.title = osc_title;
79 pane.has_default_title = false;
80 } else {
81 pane.title =
82 format_remote_title(hostname, username, cwd, remote_format);
83 pane.has_default_title = false;
84 }
85 } else if !osc_title.is_empty() {
86 pane.title = osc_title;
87 pane.has_default_title = false;
88 } else if title_mode == par_term_config::TabTitleMode::Auto
89 && let Some(cwd) = cwd
90 {
91 let abbreviated = if let Some(ref home) = home_dir {
92 cwd.replace(&home.to_string_lossy().to_string(), "~")
93 } else {
94 cwd
95 };
96 if let Some(last) = abbreviated.rsplit('/').next() {
97 if !last.is_empty() {
98 pane.title = last.to_string();
99 } else {
100 pane.title = abbreviated;
101 }
102 } else {
103 pane.title = abbreviated;
104 }
105 pane.has_default_title = false;
106 }
107 }
109 }
110 }
111 if let Some((focused_id, pm)) = focused_id.zip(self.pane_manager.as_ref())
115 && let Some(pane) = pm.get_pane(focused_id)
116 {
117 self.title = pane.title.clone();
118 self.has_default_title = pane.has_default_title;
119 }
120 }
121
122 pub fn set_default_title(&mut self, tab_number: usize) {
124 if self.has_default_title {
125 let title = format!("Tab {}", tab_number);
126 self.title = title.clone();
127 if let Some(pm) = self.pane_manager.as_mut() {
131 for pane in pm.all_panes_mut() {
132 if pane.has_default_title {
133 pane.title = title.clone();
134 }
135 }
136 }
137 }
138 }
139
140 pub fn set_title(&mut self, title: &str) {
144 self.title = title.to_string();
145 self.has_default_title = false;
146 if let Some(pane) = self
148 .pane_manager
149 .as_mut()
150 .and_then(|pm| pm.focused_pane_mut())
151 {
152 pane.title = title.to_string();
153 pane.has_default_title = false;
154 }
155 }
156
157 pub fn is_running(&self) -> bool {
159 if let Ok(term) = self.terminal.try_read() {
160 term.is_running()
161 } else {
162 true }
164 }
165
166 pub fn get_cwd(&self) -> Option<String> {
168 if let Ok(term) = self.terminal.try_read() {
169 term.shell_integration_cwd()
170 } else {
171 self.working_directory.clone()
172 }
173 }
174
175 pub fn set_custom_color(&mut self, color: [u8; 3]) {
177 self.custom_color = Some(color);
178 }
179
180 pub fn clear_custom_color(&mut self) {
182 self.custom_color = None;
183 }
184
185 pub fn has_custom_color(&self) -> bool {
187 self.custom_color.is_some()
188 }
189
190 pub fn parse_hostname_from_osc7_url(url: &str) -> Option<String> {
195 let path = url.strip_prefix("file://")?;
196
197 if path.starts_with('/') {
198 None
200 } else {
201 let hostname = path.split('/').next()?;
203 if hostname.is_empty() || hostname == "localhost" {
204 None
205 } else {
206 Some(hostname.to_string())
207 }
208 }
209 }
210
211 pub fn check_hostname_change(&mut self) -> Option<String> {
218 let current_hostname = if let Ok(term) = self.terminal.try_read() {
219 term.shell_integration_hostname()
220 } else {
221 return None;
222 };
223
224 if current_hostname != self.detected_hostname {
226 let old_hostname = self.detected_hostname.take();
227 self.detected_hostname = current_hostname.clone();
228
229 crate::debug_info!(
230 "PROFILE",
231 "Hostname changed: {:?} -> {:?}",
232 old_hostname,
233 current_hostname
234 );
235
236 current_hostname
238 } else {
239 None
240 }
241 }
242
243 pub fn check_cwd_change(&mut self) -> Option<String> {
248 let current_cwd = self.get_cwd();
249
250 if current_cwd != self.detected_cwd {
251 let old_cwd = self.detected_cwd.take();
252 self.detected_cwd = current_cwd.clone();
253
254 crate::debug_info!("PROFILE", "CWD changed: {:?} -> {:?}", old_cwd, current_cwd);
255
256 current_cwd
257 } else {
258 None
259 }
260 }
261
262 pub fn clear_auto_profile(&mut self) {
267 self.profile.auto_applied_profile_id = None;
268 self.profile.auto_applied_dir_profile_id = None;
269 self.profile.profile_icon = None;
270 if let Some(original) = self.profile.pre_profile_title.take() {
271 self.set_title(&original);
272 }
273 self.profile.badge_override = None;
274 }
275}
276
277fn format_remote_title(
283 hostname: Option<String>,
284 username: Option<String>,
285 cwd: Option<String>,
286 format: par_term_config::RemoteTabTitleFormat,
287) -> String {
288 use par_term_config::RemoteTabTitleFormat;
289 let host = hostname.unwrap_or_default();
290 match format {
291 RemoteTabTitleFormat::UserAtHost => {
292 if let Some(user) = username {
293 format!("{}@{}", user, host)
294 } else {
295 host
296 }
297 }
298 RemoteTabTitleFormat::Host => host,
299 RemoteTabTitleFormat::HostAndCwd => {
300 if let Some(cwd) = cwd {
301 let abbrev = if let Some(ref user) = username {
302 let linux_home = format!("/home/{}", user);
303 let macos_home = format!("/Users/{}", user);
304 let abbrev_with = |home: &str| -> Option<String> {
305 if cwd == home {
306 Some("~".to_string())
307 } else if cwd.starts_with(&format!("{}/", home)) {
308 Some(format!("~{}", &cwd[home.len()..]))
309 } else {
310 None
311 }
312 };
313 if let Some(a) = abbrev_with(&linux_home) {
314 a
315 } else if let Some(a) = abbrev_with(&macos_home) {
316 a
317 } else {
318 cwd
319 }
320 } else {
321 cwd
322 };
323 format!("{}:{}", host, abbrev)
324 } else {
325 host
326 }
327 }
328 }
329}
330
331#[cfg(test)]
332mod format_remote_title_tests {
333 use super::format_remote_title;
334 use par_term_config::RemoteTabTitleFormat;
335
336 #[test]
337 fn user_at_host_with_both() {
338 let result = format_remote_title(
339 Some("server".into()),
340 Some("alice".into()),
341 None,
342 RemoteTabTitleFormat::UserAtHost,
343 );
344 assert_eq!(result, "alice@server");
345 }
346
347 #[test]
348 fn user_at_host_no_username_falls_back_to_host() {
349 let result = format_remote_title(
350 Some("server".into()),
351 None,
352 None,
353 RemoteTabTitleFormat::UserAtHost,
354 );
355 assert_eq!(result, "server");
356 }
357
358 #[test]
359 fn host_only() {
360 let result = format_remote_title(
361 Some("mybox".into()),
362 Some("bob".into()),
363 Some("/home/bob/projects".into()),
364 RemoteTabTitleFormat::Host,
365 );
366 assert_eq!(result, "mybox");
367 }
368
369 #[test]
370 fn host_and_cwd_abbreviates_linux_home() {
371 let result = format_remote_title(
372 Some("server".into()),
373 Some("alice".into()),
374 Some("/home/alice/projects/foo".into()),
375 RemoteTabTitleFormat::HostAndCwd,
376 );
377 assert_eq!(result, "server:~/projects/foo");
378 }
379
380 #[test]
381 fn host_and_cwd_abbreviates_macos_home() {
382 let result = format_remote_title(
383 Some("mac".into()),
384 Some("alice".into()),
385 Some("/Users/alice/dev".into()),
386 RemoteTabTitleFormat::HostAndCwd,
387 );
388 assert_eq!(result, "mac:~/dev");
389 }
390
391 #[test]
392 fn host_and_cwd_no_cwd_falls_back_to_host() {
393 let result = format_remote_title(
394 Some("server".into()),
395 Some("alice".into()),
396 None,
397 RemoteTabTitleFormat::HostAndCwd,
398 );
399 assert_eq!(result, "server");
400 }
401
402 #[test]
403 fn host_and_cwd_unknown_path_no_abbreviation() {
404 let result = format_remote_title(
405 Some("server".into()),
406 Some("alice".into()),
407 Some("/var/log".into()),
408 RemoteTabTitleFormat::HostAndCwd,
409 );
410 assert_eq!(result, "server:/var/log");
411 }
412
413 #[test]
414 fn host_and_cwd_does_not_abbreviate_partial_username_match() {
415 let result = format_remote_title(
416 Some("server".into()),
417 Some("alice".into()),
418 Some("/home/alice2/projects".into()),
419 RemoteTabTitleFormat::HostAndCwd,
420 );
421 assert_eq!(result, "server:/home/alice2/projects");
422 }
423
424 #[test]
425 fn host_and_cwd_exact_home_dir_shows_tilde() {
426 let result = format_remote_title(
427 Some("server".into()),
428 Some("alice".into()),
429 Some("/home/alice".into()),
430 RemoteTabTitleFormat::HostAndCwd,
431 );
432 assert_eq!(result, "server:~");
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use crate::tab::Tab;
439
440 #[test]
441 fn test_parse_hostname_from_osc7_url_localhost() {
442 assert_eq!(Tab::parse_hostname_from_osc7_url("file:///home/user"), None);
444 assert_eq!(Tab::parse_hostname_from_osc7_url("file:///"), None);
445 assert_eq!(
446 Tab::parse_hostname_from_osc7_url("file:///var/log/syslog"),
447 None
448 );
449 }
450
451 #[test]
452 fn test_parse_hostname_from_osc7_url_remote() {
453 assert_eq!(
455 Tab::parse_hostname_from_osc7_url("file://server.example.com/home/user"),
456 Some("server.example.com".to_string())
457 );
458 assert_eq!(
459 Tab::parse_hostname_from_osc7_url("file://myhost/tmp"),
460 Some("myhost".to_string())
461 );
462 assert_eq!(
463 Tab::parse_hostname_from_osc7_url("file://192.168.1.100/var/log"),
464 Some("192.168.1.100".to_string())
465 );
466 }
467
468 #[test]
469 fn test_parse_hostname_from_osc7_url_localhost_explicit() {
470 assert_eq!(
472 Tab::parse_hostname_from_osc7_url("file://localhost/home/user"),
473 None
474 );
475 }
476
477 #[test]
478 fn test_parse_hostname_from_osc7_url_invalid() {
479 assert_eq!(Tab::parse_hostname_from_osc7_url(""), None);
481 assert_eq!(
482 Tab::parse_hostname_from_osc7_url("http://example.com"),
483 None
484 );
485 assert_eq!(Tab::parse_hostname_from_osc7_url("/home/user"), None);
486 assert_eq!(Tab::parse_hostname_from_osc7_url("file://"), None);
487 }
488
489 #[test]
490 fn test_parse_hostname_from_osc7_url_edge_cases() {
491 assert_eq!(Tab::parse_hostname_from_osc7_url("file:///"), None);
493
494 assert_eq!(
496 Tab::parse_hostname_from_osc7_url("file://host"),
497 Some("host".to_string())
498 );
499 }
500}
501
502#[cfg(test)]
503mod default_title_tests {
504 use crate::tab::Tab;
505
506 #[test]
509 fn set_default_title_syncs_pane_title() {
510 let mut tab = Tab::new_stub(1, 1);
511 {
513 let pm = tab.pane_manager.as_ref().unwrap();
514 let pane = pm.focused_pane().unwrap();
515 assert!(pane.has_default_title);
516 assert_eq!(pane.title, "");
517 }
518 tab.set_default_title(3);
519 assert_eq!(tab.title, "Tab 3");
520 let pm = tab.pane_manager.as_ref().unwrap();
522 let pane = pm.focused_pane().unwrap();
523 assert_eq!(pane.title, "Tab 3");
524 assert!(pane.has_default_title);
525 }
526
527 #[test]
529 fn set_default_title_skips_non_default_panes() {
530 let mut tab = Tab::new_stub(1, 1);
531 {
533 let pm = tab.pane_manager.as_mut().unwrap();
534 let pane = pm.focused_pane_mut().unwrap();
535 pane.title = "vim".to_string();
536 pane.has_default_title = false;
537 }
538 tab.has_default_title = true;
541 tab.set_default_title(2);
542 let pm = tab.pane_manager.as_ref().unwrap();
544 let pane = pm.focused_pane().unwrap();
545 assert_eq!(pane.title, "vim");
546 assert!(!pane.has_default_title);
547 }
548
549 #[test]
550 fn set_title_syncs_focused_pane() {
551 let mut tab = Tab::new_stub(1, 1);
552 tab.set_title("my-session");
553 assert_eq!(tab.title, "my-session");
554 assert!(!tab.has_default_title);
555 let pm = tab.pane_manager.as_ref().unwrap();
556 let pane = pm.focused_pane().unwrap();
557 assert_eq!(pane.title, "my-session");
558 assert!(!pane.has_default_title);
559 }
560}