par_term/app/tab_ops/
lifecycle.rs1use std::sync::Arc;
6
7use super::super::window_state::WindowState;
8use super::ClosedTabInfo;
9
10impl WindowState {
11 pub fn new_tab_or_show_profiles(&mut self) {
13 if self.config.load().tabs.new_tab_shortcut_shows_profiles
14 && !self.overlay_ui.profile_manager.is_empty()
15 {
16 self.tab_bar_ui.show_new_tab_profile_menu = !self.tab_bar_ui.show_new_tab_profile_menu;
17 self.request_redraw();
18 log::info!("Toggled new-tab profile menu via shortcut");
19 } else {
20 self.new_tab();
21 log::info!("New tab created");
22 }
23 }
24
25 pub fn new_tab(&mut self) {
27 if self.config.load().tabs.max_tabs > 0
29 && self.tab_manager.tab_count() >= self.config.load().tabs.max_tabs
30 {
31 log::warn!(
32 "Cannot create new tab: max_tabs limit ({}) reached",
33 self.config.load().tabs.max_tabs
34 );
35 return;
36 }
37
38 let old_tab_count = self.tab_manager.tab_count();
40
41 let prior_active_idx = self.tab_manager.active_tab_index();
43
44 let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
47
48 match self.tab_manager.new_tab(
49 &self.config.load(),
50 Arc::clone(&self.runtime),
51 self.config.load().tabs.tab_inherit_cwd,
52 grid_size,
53 ) {
54 Ok(tab_id) => {
55 if self.config.load().tabs.new_tab_position
57 == crate::config::NewTabPosition::AfterActive
58 && let Some(idx) = prior_active_idx
59 {
60 self.tab_manager.move_tab_to_index(tab_id, idx + 1);
61 }
62
63 let new_tab_count = self.tab_manager.tab_count();
65 let old_tab_bar_height = self
66 .tab_bar_ui
67 .get_height(old_tab_count, &self.config.load());
68 let new_tab_bar_height = self
69 .tab_bar_ui
70 .get_height(new_tab_count, &self.config.load());
71 let old_tab_bar_width = self
72 .tab_bar_ui
73 .get_width(old_tab_count, &self.config.load());
74 let new_tab_bar_width = self
75 .tab_bar_ui
76 .get_width(new_tab_count, &self.config.load());
77
78 if ((new_tab_bar_height - old_tab_bar_height).abs() > 0.1
80 || (new_tab_bar_width - old_tab_bar_width).abs() > 0.1)
81 && let Some(renderer) = &mut self.renderer
82 && let Some((new_cols, new_rows)) = Self::apply_tab_bar_offsets_for_position(
83 self.config.load().tabs.tab_bar_position,
84 renderer,
85 new_tab_bar_height,
86 new_tab_bar_width,
87 )
88 {
89 let cell_width = renderer.cell_width();
90 let cell_height = renderer.cell_height();
91 let width_px = (new_cols as f32 * cell_width) as usize;
92 let height_px = (new_rows as f32 * cell_height) as usize;
93
94 for tab in self.tab_manager.tabs_mut() {
96 if tab.id != tab_id {
97 if let Ok(mut term) = tab.terminal.try_write() {
101 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
102 if let Err(e) =
103 term.resize_with_pixels(new_cols, new_rows, width_px, height_px)
104 {
105 crate::debug_error!(
106 "TERMINAL",
107 "resize_with_pixels failed (new_tab): {e}"
108 );
109 }
110 }
111 tab.active_cache_mut().cells = None;
112 }
113 }
114 log::info!(
115 "Tab bar appeared (position={:?}), resized existing tabs to {}x{}",
116 self.config.load().tabs.tab_bar_position,
117 new_cols,
118 new_rows
119 );
120 }
121
122 if let Some(window) = &self.window
124 && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
125 {
126 tab.start_refresh_task(
127 Arc::clone(&self.runtime),
128 Arc::clone(window),
129 self.config.load().rendering.max_fps,
130 self.config.load().power.inactive_tab_fps,
131 );
132
133 if let Some(renderer) = &self.renderer
139 && let Ok(mut term) = tab.terminal.try_write()
140 {
141 let (cols, rows) = renderer.grid_size();
142 let cell_width = renderer.cell_width();
143 let cell_height = renderer.cell_height();
144 let width_px = (cols as f32 * cell_width) as usize;
145 let height_px = (rows as f32 * cell_height) as usize;
146
147 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
149
150 if let Err(e) = term.resize_with_pixels(cols, rows, width_px, height_px) {
152 crate::debug_error!(
153 "TERMINAL",
154 "resize_with_pixels failed (new_tab_init): {e}"
155 );
156 }
157 log::info!(
158 "Resized new tab {} terminal to {}x{} ({}x{} px)",
159 tab_id,
160 cols,
161 rows,
162 width_px,
163 height_px
164 );
165 }
166 }
167
168 self.play_alert_sound(crate::config::AlertEvent::NewTab);
170
171 self.focus_state.needs_redraw = true;
172 self.request_redraw();
173 }
174 Err(e) => {
175 log::error!("Failed to create new tab: {}", e);
176 }
177 }
178 }
179
180 pub fn close_current_tab(&mut self) -> bool {
183 log::info!(
184 "[CLOSE_TAB] close_current_tab called, confirm_close_running_jobs={}",
185 self.config.load().shell.confirm_close_running_jobs
186 );
187
188 if self.config.load().shell.confirm_close_running_jobs
190 && let Some(command_name) = self.check_current_tab_running_job()
191 && let Some(tab) = self.tab_manager.active_tab()
192 {
193 let tab_id = tab.id;
194 let tab_title = if tab.title.is_empty() {
195 "Terminal".to_string()
196 } else {
197 tab.title.clone()
198 };
199 log::info!(
200 "[CLOSE_TAB] Showing close confirmation for tab {} with running command: {}",
201 tab_id,
202 command_name
203 );
204 self.overlay_ui
205 .close_confirmation_ui
206 .show_for_tab(tab_id, &tab_title, &command_name);
207 self.focus_state.needs_redraw = true;
208 self.request_redraw();
209 return false; }
211
212 log::info!(
213 "[CLOSE_TAB] No running job detected or confirmation disabled, closing immediately"
214 );
215 self.close_current_tab_immediately()
216 }
217
218 pub fn close_current_tab_immediately(&mut self) -> bool {
221 if let Some(tab_id) = self.tab_manager.active_tab_id() {
222 let is_tmux_gateway = self.config.load().tmux.tmux_enabled
226 && self.tmux_state.tmux_gateway_tab_id == Some(tab_id)
227 && self.is_gateway_active();
228 if is_tmux_gateway {
229 self.write_to_gateway("detach-client\n");
230 self.disconnect_tmux_session();
231 }
232
233 let is_tmux_display_tab = self.config.load().tmux.tmux_enabled
236 && self.tmux_state.tmux_gateway_tab_id.is_some()
237 && self.tmux_state.tmux_gateway_tab_id != Some(tab_id)
238 && self.is_gateway_active();
239
240 let old_tab_count = self.tab_manager.tab_count();
242 let old_tab_bar_height = self
243 .tab_bar_ui
244 .get_height(old_tab_count, &self.config.load());
245 let old_tab_bar_width = self
246 .tab_bar_ui
247 .get_width(old_tab_count, &self.config.load());
248
249 let is_last_tab = self.tab_manager.tab_count() <= 1;
250 let preserve_shell = self
251 .config
252 .load()
253 .session_restore
254 .session_undo_preserve_shell
255 && self.config.load().session_restore.session_undo_timeout_secs > 0;
256
257 let is_last = if preserve_shell {
259 if let Some(tab) = self.tab_manager.get_tab(tab_id) {
261 let cwd = tab.get_cwd();
262 let title = tab.title.clone();
263 let has_default_title = tab.has_default_title;
264 let custom_color = tab.custom_color;
265 let index = self.tab_manager.active_tab_index().unwrap_or(0);
266
267 if let Some((mut hidden_tab, is_empty)) = self.tab_manager.remove_tab(tab_id) {
268 hidden_tab.stop_refresh_task();
270
271 let info = ClosedTabInfo {
272 cwd,
273 title,
274 has_default_title,
275 index,
276 closed_at: std::time::Instant::now(),
277 pane_layout: None, custom_color,
279 hidden_tab: Some(hidden_tab),
280 };
281 self.overlay_state.closed_tabs.push_front(info);
282 while self.overlay_state.closed_tabs.len()
283 > self.config.load().session_restore.session_undo_max_entries
284 {
285 self.overlay_state.closed_tabs.pop_back();
286 }
287 is_empty
288 } else {
289 self.tab_manager.close_tab(tab_id)
291 }
292 } else {
293 self.tab_manager.close_tab(tab_id)
294 }
295 } else {
296 if self.config.load().session_restore.session_undo_timeout_secs > 0
298 && let Some(tab) = self.tab_manager.get_tab(tab_id)
299 {
300 let info = ClosedTabInfo {
301 cwd: tab.get_cwd(),
302 title: tab.title.clone(),
303 has_default_title: tab.has_default_title,
304 index: self.tab_manager.active_tab_index().unwrap_or(0),
305 closed_at: std::time::Instant::now(),
306 pane_layout: tab
307 .pane_manager
308 .as_ref()
309 .and_then(|pm| pm.root())
310 .map(crate::session::capture::capture_pane_node),
311 custom_color: tab.custom_color,
312 hidden_tab: None,
313 };
314 self.overlay_state.closed_tabs.push_front(info);
315 while self.overlay_state.closed_tabs.len()
316 > self.config.load().session_restore.session_undo_max_entries
317 {
318 self.overlay_state.closed_tabs.pop_back();
319 }
320 }
321
322 self.tab_manager.close_tab(tab_id)
323 };
324
325 if is_tmux_display_tab && self.is_gateway_active() {
329 let gateway_tab_id = self.tmux_state.tmux_gateway_tab_id;
330 let has_other_visible = self
331 .tab_manager
332 .tabs()
333 .iter()
334 .any(|t| !t.is_hidden && Some(t.id) != gateway_tab_id);
335 if !has_other_visible {
336 self.show_gateway_tab();
337 if let Some(gtid) = gateway_tab_id {
339 self.tab_manager.switch_to(gtid);
340 }
341 }
342 }
343
344 self.play_alert_sound(crate::config::AlertEvent::TabClose);
346
347 if !is_last_tab {
349 let key_hint = self
350 .config
351 .load()
352 .keybindings
353 .iter()
354 .find(|kb| kb.action == "reopen_closed_tab")
355 .map(|kb| kb.key.clone())
356 .unwrap_or_else(|| "keybinding".to_string());
357 let timeout = self.config.load().session_restore.session_undo_timeout_secs;
358 if timeout > 0 {
359 self.show_toast(format!(
360 "Tab closed. Press {} to undo ({timeout}s)",
361 key_hint
362 ));
363 }
364 }
365
366 if !is_last {
368 let new_tab_count = self.tab_manager.tab_count();
369 let new_tab_bar_height = self
370 .tab_bar_ui
371 .get_height(new_tab_count, &self.config.load());
372 let new_tab_bar_width = self
373 .tab_bar_ui
374 .get_width(new_tab_count, &self.config.load());
375
376 if ((new_tab_bar_height - old_tab_bar_height).abs() > 0.1
377 || (new_tab_bar_width - old_tab_bar_width).abs() > 0.1)
378 && let Some(renderer) = &mut self.renderer
379 && let Some((new_cols, new_rows)) = Self::apply_tab_bar_offsets_for_position(
380 self.config.load().tabs.tab_bar_position,
381 renderer,
382 new_tab_bar_height,
383 new_tab_bar_width,
384 )
385 {
386 let cell_width = renderer.cell_width();
387 let cell_height = renderer.cell_height();
388 let width_px = (new_cols as f32 * cell_width) as usize;
389 let height_px = (new_rows as f32 * cell_height) as usize;
390
391 for tab in self.tab_manager.tabs_mut() {
393 if let Ok(mut term) = tab.terminal.try_write() {
396 term.set_cell_dimensions(cell_width as u32, cell_height as u32);
397 if let Err(e) =
398 term.resize_with_pixels(new_cols, new_rows, width_px, height_px)
399 {
400 crate::debug_error!(
401 "TERMINAL",
402 "resize_with_pixels failed (close_tab): {e}"
403 );
404 }
405 }
406 tab.active_cache_mut().cells = None;
407 }
408 log::info!(
409 "Tab bar visibility changed (position={:?}), resized remaining tabs to {}x{}",
410 self.config.load().tabs.tab_bar_position,
411 new_cols,
412 new_rows
413 );
414 }
415 }
416
417 self.focus_state.needs_redraw = true;
418 self.request_redraw();
419 is_last
420 } else {
421 true }
423 }
424
425 pub fn next_tab(&mut self) {
427 self.copy_mode.exit();
428 self.tab_manager.next_tab();
429 self.clear_and_invalidate();
430 }
431
432 pub fn prev_tab(&mut self) {
434 self.copy_mode.exit();
435 self.tab_manager.prev_tab();
436 self.clear_and_invalidate();
437 }
438
439 pub fn switch_to_tab_index(&mut self, index: usize) {
441 self.copy_mode.exit();
442 self.tab_manager.switch_to_index(index);
443 self.clear_and_invalidate();
444 }
445
446 pub fn move_tab_left(&mut self) {
448 self.tab_manager.move_active_tab_left();
449 self.focus_state.needs_redraw = true;
450 self.request_redraw();
451 }
452
453 pub fn move_tab_right(&mut self) {
455 self.tab_manager.move_active_tab_right();
456 self.focus_state.needs_redraw = true;
457 self.request_redraw();
458 }
459}