Skip to main content

par_term/app/tab_ops/
pane_ops.rs

1//! Split pane operations: split, navigate, resize, close panes.
2
3use std::sync::Arc;
4
5use super::super::window_state::WindowState;
6
7impl WindowState {
8    /// Shared implementation for trigger- and keyboard-initiated pane splits.
9    ///
10    /// Handles renderer bounds query, tmux delegation, and the split call.
11    /// Returns the new pane ID on success, None on failure.
12    ///
13    /// `initial_command` — when `Some((cmd, args))` the new pane launches that
14    /// process directly instead of the login shell. The pane closes when the
15    /// process exits.
16    pub(crate) fn split_pane_direction(
17        &mut self,
18        direction: crate::pane::SplitDirection,
19        focus_new: bool,
20        initial_command: Option<(String, Vec<String>)>,
21        split_percent: u8,
22    ) -> Option<crate::pane::PaneId> {
23        // Calculate status bar height for proper content area
24        let is_tmux_connected = self.is_tmux_connected();
25        let status_bar_height = crate::tmux_status_bar_ui::TmuxStatusBarUI::height(
26            &self.config.load(),
27            is_tmux_connected,
28        );
29        let custom_status_bar_height = self
30            .status_bar_ui
31            .height(&self.config.load(), self.is_fullscreen);
32
33        // Get bounds info from renderer for proper pane sizing
34        let bounds_info = self.renderer.as_ref().map(|r| {
35            let size = r.size();
36            let padding = r.window_padding();
37            let content_offset_y = r.content_offset_y();
38            let cell_width = r.cell_width();
39            let cell_height = r.cell_height();
40            let scale = r.scale_factor();
41            (
42                size,
43                padding,
44                content_offset_y,
45                cell_width,
46                cell_height,
47                scale,
48            )
49        });
50
51        let dpi_scale = bounds_info.map(|b| b.5).unwrap_or(1.0);
52
53        let tab = self.tab_manager.active_tab_mut()?;
54
55        // Set pane bounds before split if we have renderer info
56        if let Some((size, padding, content_offset_y, cell_width, cell_height, scale)) = bounds_info
57        {
58            // After split there will be multiple panes, so use 0 padding if configured
59            let effective_padding = if self.config.load().window.hide_window_padding_on_split {
60                0.0
61            } else {
62                padding
63            };
64            // Scale status_bar_height from logical to physical pixels
65            let physical_status_bar_height = (status_bar_height + custom_status_bar_height) * scale;
66            let content_width = size.width as f32 - effective_padding * 2.0;
67            let content_height = size.height as f32
68                - content_offset_y
69                - effective_padding
70                - physical_status_bar_height;
71            let bounds = crate::pane::PaneBounds::new(
72                effective_padding,
73                content_offset_y,
74                content_width,
75                content_height,
76            );
77            tab.set_pane_bounds(bounds, cell_width, cell_height);
78        }
79
80        let result = match direction {
81            crate::pane::SplitDirection::Horizontal => tab.split_horizontal(
82                focus_new,
83                &self.config.load(),
84                Arc::clone(&self.runtime),
85                dpi_scale,
86                initial_command,
87                split_percent,
88            ),
89            crate::pane::SplitDirection::Vertical => tab.split_vertical(
90                focus_new,
91                &self.config.load(),
92                Arc::clone(&self.runtime),
93                dpi_scale,
94                initial_command,
95                split_percent,
96            ),
97        };
98
99        match result {
100            Ok(Some(pane_id)) => {
101                log::info!("Split pane {:?}, new pane {}", direction, pane_id);
102                // Clear renderer cells to remove stale single-pane data
103                if let Some(renderer) = &mut self.renderer {
104                    renderer.clear_all_cells();
105                }
106                // Invalidate tab cache — must re-borrow since we moved tab above
107                if let Some(tab) = self.tab_manager.active_tab_mut() {
108                    tab.active_cache_mut().cells = None;
109                }
110                // Start refresh tasks for all panes so secondary panes trigger redraws
111                if let Some(window) = &self.window
112                    && let Some(tab) = self.tab_manager.active_tab_mut()
113                {
114                    tab.start_pane_refresh_tasks(
115                        Arc::clone(&self.runtime),
116                        Arc::clone(window),
117                        self.config.load().rendering.max_fps,
118                        self.config.load().power.inactive_tab_fps,
119                    );
120                }
121                self.focus_state.needs_redraw = true;
122                self.request_redraw();
123                Some(pane_id)
124            }
125            Ok(None) => {
126                log::info!(
127                    "{:?} split not yet functional (renderer integration pending)",
128                    direction
129                );
130                None
131            }
132            Err(e) => {
133                log::error!("Failed to split pane {:?}: {}", direction, e);
134                None
135            }
136        }
137    }
138
139    /// Split the current pane horizontally (panes stacked top/bottom)
140    pub fn split_pane_horizontal(&mut self) {
141        // In tmux mode, send split command to tmux instead
142        if self.is_tmux_connected() && self.split_pane_via_tmux(false) {
143            crate::debug_info!("TMUX", "Sent horizontal split command to tmux");
144            return;
145        }
146        // Fall through to local split if tmux command failed or not connected
147        self.split_pane_direction(crate::pane::SplitDirection::Horizontal, true, None, 50);
148    }
149
150    /// Split the current pane vertically (panes side by side)
151    pub fn split_pane_vertical(&mut self) {
152        // In tmux mode, send split command to tmux instead
153        if self.is_tmux_connected() && self.split_pane_via_tmux(true) {
154            crate::debug_info!("TMUX", "Sent vertical split command to tmux");
155            return;
156        }
157        // Fall through to local split if tmux command failed or not connected
158        self.split_pane_direction(crate::pane::SplitDirection::Vertical, true, None, 50);
159    }
160
161    /// Close the focused pane in the current tab
162    ///
163    /// If this is the last pane, the tab is closed.
164    /// Returns true if the window should close (last tab was closed).
165    pub fn close_focused_pane(&mut self) -> bool {
166        if self.is_tmux_connected() {
167            // Display tabs show tmux window content but are not the gateway.  Closing
168            // a pane via par-term's UI on a display tab should close the display tab
169            // itself — not kill the underlying tmux pane.  Killing tmux panes is tmux's
170            // responsibility (prefix+x, etc.).
171            let active_tab_id = self.tab_manager.active_tab_id();
172            let is_tmux_display_tab = active_tab_id
173                .map(|id| {
174                    self.tmux_state.tmux_gateway_tab_id.is_some()
175                        && self.tmux_state.tmux_gateway_tab_id != Some(id)
176                })
177                .unwrap_or(false);
178
179            if is_tmux_display_tab {
180                crate::debug_info!(
181                    "TMUX",
182                    "close_focused_pane on display tab — closing display tab instead of kill-pane"
183                );
184                return self.close_current_tab_immediately();
185            }
186
187            // Gateway tab (or connected but display tab check failed): send kill-pane.
188            if self.close_pane_via_tmux() {
189                crate::debug_info!("TMUX", "Sent kill-pane command to tmux");
190                // Don't close the local pane - wait for tmux layout change
191                return false;
192            }
193        }
194        // Fall through to local close if tmux command failed or not connected
195
196        // Check if we need to show confirmation for running jobs
197        if self.config.load().shell.confirm_close_running_jobs
198            && let Some(command_name) = self.check_current_pane_running_job()
199            && let Some(tab) = self.tab_manager.active_tab()
200            && let Some(pane_id) = tab.focused_pane_id()
201        {
202            let tab_id = tab.id;
203            let tab_title = if tab.title.is_empty() {
204                "Terminal".to_string()
205            } else {
206                tab.title.clone()
207            };
208            self.overlay_ui.close_confirmation_ui.show_for_pane(
209                tab_id,
210                pane_id,
211                &tab_title,
212                &command_name,
213            );
214            self.focus_state.needs_redraw = true;
215            self.request_redraw();
216            return false; // Don't close yet, waiting for confirmation
217        }
218
219        self.close_focused_pane_immediately()
220    }
221
222    /// Close the focused pane immediately without confirmation
223    /// Returns true if the window should close (last tab was closed).
224    pub(crate) fn close_focused_pane_immediately(&mut self) -> bool {
225        if let Some(tab) = self.tab_manager.active_tab_mut()
226            && tab.has_multiple_panes()
227        {
228            let is_last_pane = tab.close_focused_pane();
229            if is_last_pane {
230                // Last pane closed, close the tab
231                return self.close_current_tab_immediately();
232            }
233            self.focus_state.needs_redraw = true;
234            self.request_redraw();
235            return false;
236        }
237        // Single pane or no tab, close the tab
238        self.close_current_tab_immediately()
239    }
240
241    /// Check if the current pane's terminal has a running job that should trigger confirmation
242    ///
243    /// Returns Some(command_name) if confirmation should be shown, None otherwise.
244    pub(super) fn check_current_pane_running_job(&self) -> Option<String> {
245        let tab = self.tab_manager.active_tab()?;
246
247        // If the tab has split panes, check the focused pane
248        if tab.has_multiple_panes() {
249            let pane_manager = tab.pane_manager()?;
250            let focused_id = pane_manager.focused_pane_id()?;
251            let pane = pane_manager.get_pane(focused_id)?;
252            // blocking_read: user-initiated close — must not silently skip confirmation.
253            // should_confirm_close() only needs &self; read lock is correct.
254            let term = pane.terminal.blocking_read();
255            log::info!(
256                "[CLOSE_CONFIRM] check_current_pane_running_job (split pane): marker={:?} command={:?}",
257                term.shell_integration_marker(),
258                term.shell_integration_command()
259            );
260            return term.should_confirm_close(&self.config.load().shell.jobs_to_ignore);
261        }
262
263        // Single pane - use the tab's terminal.
264        // blocking_read: user-initiated close — must not silently skip confirmation.
265        let term = tab.terminal.blocking_read();
266        log::info!(
267            "[CLOSE_CONFIRM] check_current_pane_running_job (single pane): marker={:?} command={:?} is_running={}",
268            term.shell_integration_marker(),
269            term.shell_integration_command(),
270            term.is_command_running()
271        );
272        term.should_confirm_close(&self.config.load().shell.jobs_to_ignore)
273    }
274
275    /// Check if the current tab has multiple panes
276    pub fn has_multiple_panes(&self) -> bool {
277        self.tab_manager
278            .active_tab()
279            .is_some_and(|tab| tab.has_multiple_panes())
280    }
281
282    /// Navigate to an adjacent pane in the given direction
283    pub fn navigate_pane(&mut self, direction: crate::pane::NavigationDirection) {
284        if let Some(tab) = self.tab_manager.active_tab_mut()
285            && tab.has_multiple_panes()
286        {
287            tab.navigate_pane(direction);
288            self.focus_state.needs_redraw = true;
289            self.request_redraw();
290        }
291    }
292
293    /// Resize the focused pane in the given direction
294    ///
295    /// Growing left/up decreases the pane's ratio, growing right/down increases it
296    pub fn resize_pane(&mut self, direction: crate::pane::NavigationDirection) {
297        use crate::pane::NavigationDirection;
298
299        // Resize step: 5% per keypress
300        const RESIZE_DELTA: f32 = 0.05;
301
302        // Determine delta based on direction
303        // Right/Down: grow focused pane (positive delta)
304        // Left/Up: shrink focused pane (negative delta)
305        let delta = match direction {
306            NavigationDirection::Right | NavigationDirection::Down => RESIZE_DELTA,
307            NavigationDirection::Left | NavigationDirection::Up => -RESIZE_DELTA,
308        };
309
310        if let Some(tab) = self.tab_manager.active_tab_mut()
311            && let Some(pm) = tab.pane_manager_mut()
312            && let Some(focused_id) = pm.focused_pane_id()
313        {
314            pm.resize_split(focused_id, delta);
315            self.focus_state.needs_redraw = true;
316            self.request_redraw();
317        }
318    }
319}