Skip to main content

par_term/app/tab_ops/
tab_reopen.rs

1//! Tab reopen (session undo) and tab-bar resize helpers.
2//!
3//! Extracted from `lifecycle` to keep create/close logic separate from
4//! the undo-stack / preserved-shell reopen path.
5//!
6//! Contains:
7//! - `reopen_closed_tab` — restore most-recently-closed tab from undo stack
8//! - `handle_tab_bar_resize_after_add` — resize existing tabs when tab bar appears
9
10use std::sync::Arc;
11
12use super::super::window_state::WindowState;
13
14impl WindowState {
15    /// Reopen the most recently closed tab at its original position
16    pub fn reopen_closed_tab(&mut self) {
17        // Prune expired entries
18        if self.config.load().session_restore.session_undo_timeout_secs > 0 {
19            let timeout = std::time::Duration::from_secs(
20                self.config.load().session_restore.session_undo_timeout_secs as u64,
21            );
22            let now = std::time::Instant::now();
23            self.overlay_state
24                .closed_tabs
25                .retain(|info| now.duration_since(info.closed_at) < timeout);
26        }
27
28        let info = match self.overlay_state.closed_tabs.pop_front() {
29            Some(info) => info,
30            None => {
31                self.show_toast("No recently closed tabs");
32                return;
33            }
34        };
35
36        // Check max tabs limit
37        if self.config.load().tabs.max_tabs > 0
38            && self.tab_manager.tab_count() >= self.config.load().tabs.max_tabs
39        {
40            log::warn!(
41                "Cannot reopen tab: max_tabs limit ({}) reached",
42                self.config.load().tabs.max_tabs
43            );
44            self.show_toast("Cannot reopen tab: max tabs limit reached");
45            // Put the info back so the user can try again after closing another tab
46            self.overlay_state.closed_tabs.push_front(info);
47            return;
48        }
49
50        // Remember tab count before restoring to detect tab bar visibility change
51        let old_tab_count = self.tab_manager.tab_count();
52
53        if let Some(hidden_tab) = info.hidden_tab {
54            // Preserved shell: re-insert the live Tab.
55            //
56            // The undo entry can outlive its id: a tab moved in from another
57            // window may have claimed it while this one sat on the stack. The
58            // insert renumbers the tab in that case, so every lookup below goes
59            // through the returned id — the stale one would resize and refresh
60            // the wrong tab.
61            let tab_id = self.tab_manager.insert_tab_at(hidden_tab, info.index);
62
63            // Handle tab bar visibility change
64            self.handle_tab_bar_resize_after_add(old_tab_count, tab_id);
65
66            // Restart refresh task and resize terminal to match current window
67            if let Some(window) = &self.window
68                && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
69            {
70                tab.start_refresh_task(
71                    Arc::clone(&self.runtime),
72                    Arc::clone(window),
73                    self.config.load().rendering.max_fps,
74                    self.config.load().power.inactive_tab_fps,
75                );
76
77                // Invalidate cell cache so content is re-rendered
78                tab.active_cache_mut().cells = None;
79
80                // try_lock: intentional — tab switch resize in sync event loop.
81                // On miss: the newly active tab uses previous dimensions until next Resized.
82                if let Some(renderer) = &self.renderer
83                    && let Ok(mut term) = tab.terminal.try_write()
84                {
85                    let (cols, rows) = renderer.grid_size();
86                    let cell_width = renderer.cell_width();
87                    let cell_height = renderer.cell_height();
88                    let width_px = (cols as f32 * cell_width) as usize;
89                    let height_px = (rows as f32 * cell_height) as usize;
90                    term.set_cell_dimensions(cell_width as u32, cell_height as u32);
91                    if let Err(e) = term.resize_with_pixels(cols, rows, width_px, height_px) {
92                        crate::debug_error!(
93                            "TERMINAL",
94                            "resize_with_pixels failed (reopen_preserved): {e}"
95                        );
96                    }
97                }
98            }
99
100            self.play_alert_sound(crate::config::AlertEvent::NewTab);
101            self.show_toast("Tab restored (session preserved)");
102            self.focus_state.needs_redraw = true;
103            self.request_redraw();
104        } else {
105            // Metadata-only: create a new tab from CWD (existing behavior)
106            let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
107
108            match self.tab_manager.new_tab_with_cwd(
109                &self.config.load(),
110                Arc::clone(&self.runtime),
111                info.cwd,
112                grid_size,
113            ) {
114                Ok(tab_id) => {
115                    // Handle tab bar visibility change
116                    self.handle_tab_bar_resize_after_add(old_tab_count, tab_id);
117
118                    // Restore title and custom color
119                    if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
120                        if !info.has_default_title {
121                            tab.set_title(&info.title);
122                        }
123                        tab.custom_color = info.custom_color;
124                    }
125
126                    // Move tab to its original position
127                    self.tab_manager.move_tab_to_index(tab_id, info.index);
128
129                    // Start refresh task and resize terminal
130                    if let Some(window) = &self.window
131                        && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
132                    {
133                        tab.start_refresh_task(
134                            Arc::clone(&self.runtime),
135                            Arc::clone(window),
136                            self.config.load().rendering.max_fps,
137                            self.config.load().power.inactive_tab_fps,
138                        );
139
140                        // try_lock: intentional — new pane initialization in sync event loop.
141                        // On miss: pane terminal keeps default dimensions; fixed on next Resized.
142                        if let Some(renderer) = &self.renderer
143                            && let Ok(mut term) = tab.terminal.try_write()
144                        {
145                            let (cols, rows) = renderer.grid_size();
146                            let cell_width = renderer.cell_width();
147                            let cell_height = renderer.cell_height();
148                            let width_px = (cols as f32 * cell_width) as usize;
149                            let height_px = (rows as f32 * cell_height) as usize;
150                            term.set_cell_dimensions(cell_width as u32, cell_height as u32);
151                            if let Err(e) = term.resize_with_pixels(cols, rows, width_px, height_px)
152                            {
153                                crate::debug_error!(
154                                    "TERMINAL",
155                                    "resize_with_pixels failed (reopen_cwd): {e}"
156                                );
157                            }
158                        }
159                    }
160
161                    // Restore pane layout if present
162                    if let Some(pane_layout) = &info.pane_layout
163                        && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
164                    {
165                        tab.restore_pane_layout(
166                            pane_layout,
167                            &self.config.load(),
168                            Arc::clone(&self.runtime),
169                        );
170                        // Start refresh tasks for restored panes
171                        if let Some(window) = &self.window
172                            && let Some(tab) = self.tab_manager.get_tab_mut(tab_id)
173                        {
174                            tab.start_pane_refresh_tasks(
175                                Arc::clone(&self.runtime),
176                                Arc::clone(window),
177                                self.config.load().rendering.max_fps,
178                                self.config.load().power.inactive_tab_fps,
179                            );
180                        }
181                    }
182
183                    self.play_alert_sound(crate::config::AlertEvent::NewTab);
184                    self.show_toast("Tab restored");
185                    self.focus_state.needs_redraw = true;
186                    self.request_redraw();
187                }
188                Err(e) => {
189                    log::error!("Failed to reopen closed tab: {}", e);
190                    self.show_toast("Failed to reopen tab");
191                }
192            }
193        }
194    }
195
196    /// Handle tab bar visibility change after adding a tab.
197    /// Resizes existing tabs if the tab bar appearance changed (e.g., from 1 to 2 tabs).
198    pub(super) fn handle_tab_bar_resize_after_add(
199        &mut self,
200        old_tab_count: usize,
201        new_tab_id: crate::tab::TabId,
202    ) {
203        let new_tab_count = self.tab_manager.tab_count();
204        let old_tab_bar_height = self
205            .tab_bar_ui
206            .get_height(old_tab_count, &self.config.load());
207        let new_tab_bar_height = self
208            .tab_bar_ui
209            .get_height(new_tab_count, &self.config.load());
210        let old_tab_bar_width = self
211            .tab_bar_ui
212            .get_width(old_tab_count, &self.config.load());
213        let new_tab_bar_width = self
214            .tab_bar_ui
215            .get_width(new_tab_count, &self.config.load());
216
217        if ((new_tab_bar_height - old_tab_bar_height).abs() > 0.1
218            || (new_tab_bar_width - old_tab_bar_width).abs() > 0.1)
219            && let Some(renderer) = &mut self.renderer
220            && let Some((new_cols, new_rows)) = Self::apply_tab_bar_offsets_for_position(
221                self.config.load().tabs.tab_bar_position,
222                renderer,
223                new_tab_bar_height,
224                new_tab_bar_width,
225            )
226        {
227            let cell_width = renderer.cell_width();
228            let cell_height = renderer.cell_height();
229            let width_px = (new_cols as f32 * cell_width) as usize;
230            let height_px = (new_rows as f32 * cell_height) as usize;
231
232            for tab in self.tab_manager.tabs_mut() {
233                if tab.id != new_tab_id {
234                    // try_lock: intentional — tab bar resize loop in sync event loop.
235                    // On miss: this tab is not resized; corrected on the next Resized event.
236                    if let Ok(mut term) = tab.terminal.try_write() {
237                        term.set_cell_dimensions(cell_width as u32, cell_height as u32);
238                        if let Err(e) =
239                            term.resize_with_pixels(new_cols, new_rows, width_px, height_px)
240                        {
241                            crate::debug_error!(
242                                "TERMINAL",
243                                "resize_with_pixels failed (tab_bar_resize): {e}"
244                            );
245                        }
246                    }
247                    tab.active_cache_mut().cells = None;
248                }
249            }
250        }
251    }
252}