1use std::sync::Arc;
6
7use super::super::window_state::WindowState;
8use super::ClosedTabInfo;
9use crate::tmux::TmuxWindowId;
10use par_term_config::TabId;
11
12impl WindowState {
13 pub fn new_tab_or_show_profiles(&mut self) {
15 if self.config.load().tabs.new_tab_shortcut_shows_profiles
16 && !self.overlay_ui.profile_manager.is_empty()
17 {
18 self.tab_bar_ui.show_new_tab_profile_menu = !self.tab_bar_ui.show_new_tab_profile_menu;
19 self.request_redraw();
20 log::info!("Toggled new-tab profile menu via shortcut");
21 } else {
22 self.new_tab();
23 log::info!("New tab created");
24 }
25 }
26
27 pub(crate) fn mux_window_for_tab(&self, tab_id: TabId) -> Option<TmuxWindowId> {
31 self.tmux_state.transport.as_ref()?;
32 self.tmux_state.tmux_sync.get_window(tab_id)
33 }
34
35 pub fn new_tab(&mut self) {
37 if self.config.load().tabs.max_tabs > 0
39 && self.tab_manager.tab_count() >= self.config.load().tabs.max_tabs
40 {
41 log::warn!(
42 "Cannot create new tab: max_tabs limit ({}) reached",
43 self.config.load().tabs.max_tabs
44 );
45 return;
46 }
47
48 let sent = self
55 .tmux_state
56 .transport
57 .as_ref()
58 .map(|t| t.send_command("new-window"));
59 if let Some(result) = sent {
60 match result {
61 Ok(_) => {
62 log::info!("MUX: new-window sent; tab arrives via %window-add");
63 }
64 Err(e) => {
65 log::error!("MUX: new-window failed: {e}");
66 self.show_toast(format!("par-mux: new-window failed — {e}"));
67 }
68 }
69 self.request_redraw();
70 return;
71 }
72
73 let old_tab_count = self.tab_manager.tab_count();
75
76 let prior_active_idx = self.tab_manager.active_tab_index();
78
79 let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
82
83 match self.tab_manager.new_tab(
84 &self.config.load(),
85 Arc::clone(&self.runtime),
86 self.config.load().tabs.tab_inherit_cwd,
87 grid_size,
88 ) {
89 Ok(tab_id) => {
90 if self.config.load().tabs.new_tab_position
92 == crate::config::NewTabPosition::AfterActive
93 && let Some(idx) = prior_active_idx
94 {
95 self.tab_manager.move_tab_to_index(tab_id, idx + 1);
96 }
97
98 let new_tab_count = self.tab_manager.tab_count();
100 let old_tab_bar_height = self
101 .tab_bar_ui
102 .get_height(old_tab_count, &self.config.load());
103 let new_tab_bar_height = self
104 .tab_bar_ui
105 .get_height(new_tab_count, &self.config.load());
106 let old_tab_bar_width = self
107 .tab_bar_ui
108 .get_width(old_tab_count, &self.config.load());
109 let new_tab_bar_width = self
110 .tab_bar_ui
111 .get_width(new_tab_count, &self.config.load());
112
113 if ((new_tab_bar_height - old_tab_bar_height).abs() > 0.1
115 || (new_tab_bar_width - old_tab_bar_width).abs() > 0.1)
116 && let Some(renderer) = &mut self.renderer
117 && let Some((new_cols, new_rows)) = Self::apply_tab_bar_offsets_for_position(
118 self.config.load().tabs.tab_bar_position,
119 renderer,
120 new_tab_bar_height,
121 new_tab_bar_width,
122 )
123 {
124 let cell_width = renderer.cell_width();
125 let cell_height = renderer.cell_height();
126 let width_px = (new_cols as f32 * cell_width) as usize;
127 let height_px = (new_rows as f32 * cell_height) as usize;
128
129 for tab in self.tab_manager.tabs_mut() {
131 if tab.id != tab_id {
132 if let Ok(mut term) = tab.terminal.try_write() {
136 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
137 if let Err(e) =
138 term.resize_with_pixels(new_cols, new_rows, width_px, height_px)
139 {
140 crate::debug_error!(
141 "TERMINAL",
142 "resize_with_pixels failed (new_tab): {e}"
143 );
144 }
145 }
146 tab.active_cache_mut().cells = None;
147 }
148 }
149 log::info!(
150 "Tab bar appeared (position={:?}), resized existing tabs to {}x{}",
151 self.config.load().tabs.tab_bar_position,
152 new_cols,
153 new_rows
154 );
155 }
156
157 if let Some(window) = &self.window
159 && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
160 {
161 tab.start_refresh_task(
162 Arc::clone(&self.runtime),
163 Arc::clone(window),
164 self.config.load().rendering.max_fps,
165 self.config.load().power.inactive_tab_fps,
166 );
167
168 if let Some(renderer) = &self.renderer
174 && let Ok(mut term) = tab.terminal.try_write()
175 {
176 let (cols, rows) = renderer.grid_size();
177 let cell_width = renderer.cell_width();
178 let cell_height = renderer.cell_height();
179 let width_px = (cols as f32 * cell_width) as usize;
180 let height_px = (rows as f32 * cell_height) as usize;
181
182 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
184
185 if let Err(e) = term.resize_with_pixels(cols, rows, width_px, height_px) {
187 crate::debug_error!(
188 "TERMINAL",
189 "resize_with_pixels failed (new_tab_init): {e}"
190 );
191 }
192 log::info!(
193 "Resized new tab {} terminal to {}x{} ({}x{} px)",
194 tab_id,
195 cols,
196 rows,
197 width_px,
198 height_px
199 );
200 }
201 }
202
203 self.play_alert_sound(crate::config::AlertEvent::NewTab);
205
206 self.focus_state.needs_redraw = true;
207 self.request_redraw();
208 }
209 Err(e) => {
210 log::error!("Failed to create new tab: {}", e);
211 }
212 }
213 }
214
215 pub fn close_current_tab(&mut self) -> bool {
218 log::info!(
219 "[CLOSE_TAB] close_current_tab called, confirm_close_running_jobs={}",
220 self.config.load().shell.confirm_close_running_jobs
221 );
222
223 if self.config.load().shell.confirm_close_running_jobs
225 && let Some(command_name) = self.check_current_tab_running_job()
226 && let Some(tab) = self.tab_manager.active_tab()
227 {
228 let tab_id = tab.id;
229 let tab_title = if tab.title.is_empty() {
230 "Terminal".to_string()
231 } else {
232 tab.title.clone()
233 };
234 log::info!(
235 "[CLOSE_TAB] Showing close confirmation for tab {} with running command: {}",
236 tab_id,
237 command_name
238 );
239 self.overlay_ui
240 .close_confirmation_ui
241 .show_for_tab(tab_id, &tab_title, &command_name);
242 self.focus_state.needs_redraw = true;
243 self.request_redraw();
244 return false; }
246
247 log::info!(
248 "[CLOSE_TAB] No running job detected or confirmation disabled, closing immediately"
249 );
250 self.close_current_tab_immediately()
251 }
252
253 pub fn close_current_tab_immediately(&mut self) -> bool {
256 self.close_current_tab_inner(true)
257 }
258
259 #[cfg_attr(not(feature = "mux"), allow(dead_code))]
264 pub(crate) fn close_current_tab_keeping_mux_window(&mut self) -> bool {
265 self.close_current_tab_inner(false)
266 }
267
268 fn close_current_tab_inner(&mut self, kill_mux_window: bool) -> bool {
273 if let Some(tab_id) = self.tab_manager.active_tab_id() {
274 let is_tmux_gateway = self.config.load().tmux.tmux_enabled
278 && self.tmux_state.tmux_gateway_tab_id == Some(tab_id)
279 && self.is_gateway_active();
280 if is_tmux_gateway {
281 self.write_to_gateway("detach-client\n");
282 self.disconnect_tmux_session();
283 }
284
285 if kill_mux_window && let Some(window_id) = self.mux_window_for_tab(tab_id) {
292 let killed = self
293 .tmux_state
294 .transport
295 .as_ref()
296 .map(|t| t.send_command(&format!("kill-window -t @{window_id}")));
297 if matches!(&killed, Some(Ok(_))) {
298 log::info!("MUX: kill-window @{} sent for tab {tab_id}", window_id);
299 return false; }
304 if let Some(Err(e)) = &killed {
305 log::error!("MUX: kill-window @{window_id} failed: {e}");
306 self.show_toast(format!("par-mux: kill-window failed — {e}"));
307 }
310 }
311
312 let is_tmux_display_tab = self.config.load().tmux.tmux_enabled
315 && self.tmux_state.tmux_gateway_tab_id.is_some()
316 && self.tmux_state.tmux_gateway_tab_id != Some(tab_id)
317 && self.is_gateway_active();
318
319 let old_tab_count = self.tab_manager.tab_count();
321 let old_tab_bar_height = self
322 .tab_bar_ui
323 .get_height(old_tab_count, &self.config.load());
324 let old_tab_bar_width = self
325 .tab_bar_ui
326 .get_width(old_tab_count, &self.config.load());
327
328 let is_last_tab = self.tab_manager.tab_count() <= 1;
329 let preserve_shell = self
330 .config
331 .load()
332 .session_restore
333 .session_undo_preserve_shell
334 && self.config.load().session_restore.session_undo_timeout_secs > 0;
335
336 let is_last = if preserve_shell {
338 if let Some(tab) = self.tab_manager.get_tab(tab_id) {
340 let cwd = tab.get_cwd();
341 let title = tab.title.clone();
342 let has_default_title = tab.has_default_title;
343 let custom_color = tab.custom_color;
344 let index = self.tab_manager.active_tab_index().unwrap_or(0);
345
346 if let Some((mut hidden_tab, is_empty)) = self.tab_manager.remove_tab(tab_id) {
347 hidden_tab.stop_refresh_task();
349
350 let info = ClosedTabInfo {
351 cwd,
352 title,
353 has_default_title,
354 index,
355 closed_at: std::time::Instant::now(),
356 pane_layout: None, custom_color,
358 hidden_tab: Some(hidden_tab),
359 };
360 self.overlay_state.closed_tabs.push_front(info);
361 while self.overlay_state.closed_tabs.len()
362 > self.config.load().session_restore.session_undo_max_entries
363 {
364 self.overlay_state.closed_tabs.pop_back();
365 }
366 is_empty
367 } else {
368 self.tab_manager.close_tab(tab_id)
370 }
371 } else {
372 self.tab_manager.close_tab(tab_id)
373 }
374 } else {
375 if self.config.load().session_restore.session_undo_timeout_secs > 0
377 && let Some(tab) = self.tab_manager.get_tab(tab_id)
378 {
379 let info = ClosedTabInfo {
380 cwd: tab.get_cwd(),
381 title: tab.title.clone(),
382 has_default_title: tab.has_default_title,
383 index: self.tab_manager.active_tab_index().unwrap_or(0),
384 closed_at: std::time::Instant::now(),
385 pane_layout: tab
386 .pane_manager
387 .as_ref()
388 .and_then(|pm| pm.root())
389 .map(crate::session::capture::capture_pane_node),
390 custom_color: tab.custom_color,
391 hidden_tab: None,
392 };
393 self.overlay_state.closed_tabs.push_front(info);
394 while self.overlay_state.closed_tabs.len()
395 > self.config.load().session_restore.session_undo_max_entries
396 {
397 self.overlay_state.closed_tabs.pop_back();
398 }
399 }
400
401 self.tab_manager.close_tab(tab_id)
402 };
403
404 if is_tmux_display_tab && self.is_gateway_active() {
408 let gateway_tab_id = self.tmux_state.tmux_gateway_tab_id;
409 let has_other_visible = self
410 .tab_manager
411 .tabs()
412 .iter()
413 .any(|t| !t.is_hidden && Some(t.id) != gateway_tab_id);
414 if !has_other_visible {
415 self.show_gateway_tab();
416 if let Some(gtid) = gateway_tab_id {
418 self.tab_manager.switch_to(gtid);
419 }
420 }
421 }
422
423 self.play_alert_sound(crate::config::AlertEvent::TabClose);
425
426 if !is_last_tab {
428 let key_hint = self
429 .config
430 .load()
431 .keybindings
432 .iter()
433 .find(|kb| kb.action == "reopen_closed_tab")
434 .map(|kb| kb.key.clone())
435 .unwrap_or_else(|| "keybinding".to_string());
436 let timeout = self.config.load().session_restore.session_undo_timeout_secs;
437 if timeout > 0 {
438 self.show_toast(format!(
439 "Tab closed. Press {} to undo ({timeout}s)",
440 key_hint
441 ));
442 }
443 }
444
445 if !is_last {
447 let new_tab_count = self.tab_manager.tab_count();
448 let new_tab_bar_height = self
449 .tab_bar_ui
450 .get_height(new_tab_count, &self.config.load());
451 let new_tab_bar_width = self
452 .tab_bar_ui
453 .get_width(new_tab_count, &self.config.load());
454
455 if ((new_tab_bar_height - old_tab_bar_height).abs() > 0.1
456 || (new_tab_bar_width - old_tab_bar_width).abs() > 0.1)
457 && let Some(renderer) = &mut self.renderer
458 && let Some((new_cols, new_rows)) = Self::apply_tab_bar_offsets_for_position(
459 self.config.load().tabs.tab_bar_position,
460 renderer,
461 new_tab_bar_height,
462 new_tab_bar_width,
463 )
464 {
465 let cell_width = renderer.cell_width();
466 let cell_height = renderer.cell_height();
467 let width_px = (new_cols as f32 * cell_width) as usize;
468 let height_px = (new_rows as f32 * cell_height) as usize;
469
470 for tab in self.tab_manager.tabs_mut() {
472 if let Ok(mut term) = tab.terminal.try_write() {
475 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
476 if let Err(e) =
477 term.resize_with_pixels(new_cols, new_rows, width_px, height_px)
478 {
479 crate::debug_error!(
480 "TERMINAL",
481 "resize_with_pixels failed (close_tab): {e}"
482 );
483 }
484 }
485 tab.active_cache_mut().cells = None;
486 }
487 log::info!(
488 "Tab bar visibility changed (position={:?}), resized remaining tabs to {}x{}",
489 self.config.load().tabs.tab_bar_position,
490 new_cols,
491 new_rows
492 );
493 }
494 }
495
496 self.focus_state.needs_redraw = true;
497 self.request_redraw();
498 is_last
499 } else {
500 true }
502 }
503
504 pub fn next_tab(&mut self) {
506 self.copy_mode.exit();
507 self.tab_manager.next_tab();
508 self.clear_and_invalidate();
509 }
510
511 pub fn prev_tab(&mut self) {
513 self.copy_mode.exit();
514 self.tab_manager.prev_tab();
515 self.clear_and_invalidate();
516 }
517
518 pub fn switch_to_tab_index(&mut self, index: usize) {
520 self.copy_mode.exit();
521 self.tab_manager.switch_to_index(index);
522 self.clear_and_invalidate();
523 }
524
525 pub fn move_tab_left(&mut self) {
527 self.tab_manager.move_active_tab_left();
528 self.focus_state.needs_redraw = true;
529 self.request_redraw();
530 }
531
532 pub fn move_tab_right(&mut self) {
534 self.tab_manager.move_active_tab_right();
535 self.focus_state.needs_redraw = true;
536 self.request_redraw();
537 }
538}