Skip to main content

par_term/app/tab_ops/
lifecycle.rs

1//! Tab lifecycle operations: create, close, navigate.
2//!
3//! Reopen (session undo) and tab-bar resize helpers live in `tab_reopen`.
4
5use std::sync::Arc;
6
7use super::super::window_state::WindowState;
8use super::ClosedTabInfo;
9
10impl WindowState {
11    /// Create a new tab, or show profile picker if configured and profiles exist
12    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    /// Create a new tab
26    pub fn new_tab(&mut self) {
27        // Check max tabs limit
28        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        // Remember tab count before creating new tab to detect tab bar visibility change
39        let old_tab_count = self.tab_manager.tab_count();
40
41        // Capture BEFORE creation — tab_manager switches active to the new tab inside new_tab()
42        let prior_active_idx = self.tab_manager.active_tab_index();
43
44        // Get current grid size from renderer to pass to new tab
45        // This accounts for possible tab bar height changes
46        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                // Reposition new tab if configured
56                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                // Check if tab bar visibility changed (e.g., from 1 to 2 tabs with WhenMultiple mode)
64                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 tab bar dimensions changed, update content offsets and resize ALL existing tabs
79                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                    // Resize all EXISTING tabs (not including the new one yet)
95                    for tab in self.tab_manager.tabs_mut() {
96                        if tab.id != tab_id {
97                            // try_lock: intentional — resize during new-tab creation in sync
98                            // event loop. On miss: this tab keeps old dimensions; corrected
99                            // on the next Resized event.
100                            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                // Start refresh task for the new tab and resize to match window
123                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                    // Resize terminal to match current renderer dimensions
134                    // (which now has the correct content offset)
135                    // try_lock: intentional — new-tab initialization in sync event loop.
136                    // On miss: the new tab starts with default PTY dimensions; corrected
137                    // on the next Resized event.
138                    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                        // Set cell dimensions
148                        term.set_cell_dimensions(cell_width as u32, cell_height as u32);
149
150                        // Resize terminal to match window size
151                        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                // Play new tab alert sound if configured
169                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    /// Close the current tab
181    /// Returns true if the window should close (last tab was closed)
182    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        // Check if we need to show confirmation for running jobs
189        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; // Don't close yet, waiting for confirmation
210        }
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    /// Close the current tab immediately without confirmation
219    /// Returns true if the window should close (last tab was closed)
220    pub fn close_current_tab_immediately(&mut self) -> bool {
221        if let Some(tab_id) = self.tab_manager.active_tab_id() {
222            // If the tab being closed is the tmux gateway, send detach-client first so
223            // that tmux can cleanly detach rather than treating the disconnect as a crash
224            // (which may destroy the session if destroy-unattached is enabled).
225            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            // Track whether this is a tmux display tab (non-gateway tab that shows tmux window
234            // content) so we can restore the hidden gateway tab after it is closed.
235            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            // Remember tab count before closing to detect tab bar visibility change
241            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            // Capture closed tab metadata for session undo (before destroying the tab)
258            let is_last = if preserve_shell {
259                // Preserve mode: extract the live Tab and store it in ClosedTabInfo
260                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                        // Stop refresh task to prevent invisible redraws
269                        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, // Preserved inside the hidden Tab itself
278                            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                        // Fallback: tab disappeared between get and remove
290                        self.tab_manager.close_tab(tab_id)
291                    }
292                } else {
293                    self.tab_manager.close_tab(tab_id)
294                }
295            } else {
296                // Standard mode: capture metadata, then close (drops the Tab)
297                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            // After closing a tmux display tab, check if the hidden gateway tab needs to
326            // be restored.  This happens when the user manually closes all display tabs
327            // while still connected — the gateway tab was hidden but is the only one left.
328            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                    // Switch focus to the gateway tab so the user can interact with it
338                    if let Some(gtid) = gateway_tab_id {
339                        self.tab_manager.switch_to(gtid);
340                    }
341                }
342            }
343
344            // Play tab close alert sound if configured
345            self.play_alert_sound(crate::config::AlertEvent::TabClose);
346
347            // Show undo toast (only if not the last tab — window is closing)
348            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            // Check if tab bar visibility changed (e.g., from 2 to 1 tabs with WhenMultiple mode)
367            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                    // Resize all remaining tabs
392                    for tab in self.tab_manager.tabs_mut() {
393                        // try_lock: intentional — tab close resize in sync event loop.
394                        // On miss: tab keeps old dimensions; fixed on the next Resized event.
395                        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 // No tabs, window should close
422        }
423    }
424
425    /// Switch to next tab
426    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    /// Switch to previous tab
433    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    /// Switch to tab by index (1-based)
440    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    /// Move current tab left
447    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    /// Move current tab right
454    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}