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        // par-mux transport: split daemon-side — the daemon pane is the
142        // real one; a native split here would create a local pane whose
143        // input the mux router then starves.
144        #[cfg(feature = "mux")]
145        if self.split_pane_via_mux(false) {
146            return;
147        }
148        // In tmux mode, send split command to tmux instead
149        if self.is_tmux_connected() && self.split_pane_via_tmux(false) {
150            crate::debug_info!("TMUX", "Sent horizontal split command to tmux");
151            return;
152        }
153        // Fall through to local split if tmux command failed or not connected
154        self.split_pane_direction(crate::pane::SplitDirection::Horizontal, true, None, 50);
155    }
156
157    /// Split the current pane vertically (panes side by side)
158    pub fn split_pane_vertical(&mut self) {
159        // par-mux transport: split daemon-side — the daemon pane is the
160        // real one; a native split here would create a local pane whose
161        // input the mux router then starves.
162        #[cfg(feature = "mux")]
163        if self.split_pane_via_mux(true) {
164            return;
165        }
166        // In tmux mode, send split command to tmux instead
167        if self.is_tmux_connected() && self.split_pane_via_tmux(true) {
168            crate::debug_info!("TMUX", "Sent vertical split command to tmux");
169            return;
170        }
171        // Fall through to local split if tmux command failed or not connected
172        self.split_pane_direction(crate::pane::SplitDirection::Vertical, true, None, 50);
173    }
174
175    /// Close the focused pane in the current tab
176    ///
177    /// If this is the last pane, the tab is closed.
178    /// Returns true if the window should close (last tab was closed).
179    pub fn close_focused_pane(&mut self) -> bool {
180        // par-mux transport: close daemon-side — the daemon pane is the
181        // real one; a native close here would delete the local pane and
182        // dangle the tmux→native mapping while the daemon pane lives on.
183        #[cfg(feature = "mux")]
184        {
185            // A mux tab's LAST pane must not be kill-pane'd: the daemon's
186            // kill-pane on a window's only pane CLOSES the window (and an
187            // emptied session) — destroying the session without warning
188            // while the tab sat dead. Close the TAB instead, the Cmd+W
189            // shape: the daemon window keeps running (recorded decision,
190            // card 01a0d9b5568175c084eb7f9d70c0ec4f — the alternative,
191            // confirming before a kill, is the heavier UX for the same
192            // safety).
193            if self.tmux_state.transport.is_some()
194                && self.focused_mux_pane_from_native().is_some()
195                && !self
196                    .tab_manager
197                    .active_tab()
198                    .is_some_and(|tab| tab.has_multiple_panes())
199            {
200                crate::debug_info!("MUX", "close of a mux tab's last pane — closing the tab");
201                self.show_toast("par-mux: closed tab — window survives in the daemon");
202                return self.close_current_tab_keeping_mux_window();
203            }
204            if self.close_pane_via_mux() {
205                // Consumed: the daemon's %layout-change drives the local
206                // removal, so the window-close answer is "not yet".
207                return false;
208            }
209        }
210        if self.is_tmux_connected() {
211            // Display tabs show tmux window content but are not the gateway.  Closing
212            // a pane via par-term's UI on a display tab should close the display tab
213            // itself — not kill the underlying tmux pane.  Killing tmux panes is tmux's
214            // responsibility (prefix+x, etc.).
215            let active_tab_id = self.tab_manager.active_tab_id();
216            let is_tmux_display_tab = active_tab_id
217                .map(|id| {
218                    self.tmux_state.tmux_gateway_tab_id.is_some()
219                        && self.tmux_state.tmux_gateway_tab_id != Some(id)
220                })
221                .unwrap_or(false);
222
223            if is_tmux_display_tab {
224                crate::debug_info!(
225                    "TMUX",
226                    "close_focused_pane on display tab — closing display tab instead of kill-pane"
227                );
228                return self.close_current_tab_immediately();
229            }
230
231            // Gateway tab (or connected but display tab check failed): send kill-pane.
232            if self.close_pane_via_tmux() {
233                crate::debug_info!("TMUX", "Sent kill-pane command to tmux");
234                // Don't close the local pane - wait for tmux layout change
235                return false;
236            }
237        }
238        // Fall through to local close if tmux command failed or not connected
239
240        // Check if we need to show confirmation for running jobs
241        if self.config.load().shell.confirm_close_running_jobs
242            && let Some(command_name) = self.check_current_pane_running_job()
243            && let Some(tab) = self.tab_manager.active_tab()
244            && let Some(pane_id) = tab.focused_pane_id()
245        {
246            let tab_id = tab.id;
247            let tab_title = if tab.title.is_empty() {
248                "Terminal".to_string()
249            } else {
250                tab.title.clone()
251            };
252            self.overlay_ui.close_confirmation_ui.show_for_pane(
253                tab_id,
254                pane_id,
255                &tab_title,
256                &command_name,
257            );
258            self.focus_state.needs_redraw = true;
259            self.request_redraw();
260            return false; // Don't close yet, waiting for confirmation
261        }
262
263        self.close_focused_pane_immediately()
264    }
265
266    /// Close the focused pane immediately without confirmation
267    /// Returns true if the window should close (last tab was closed).
268    pub(crate) fn close_focused_pane_immediately(&mut self) -> bool {
269        if let Some(tab) = self.tab_manager.active_tab_mut()
270            && tab.has_multiple_panes()
271        {
272            let is_last_pane = tab.close_focused_pane();
273            if is_last_pane {
274                // Last pane closed, close the tab
275                return self.close_current_tab_immediately();
276            }
277            self.focus_state.needs_redraw = true;
278            self.request_redraw();
279            return false;
280        }
281        // Single pane or no tab, close the tab
282        self.close_current_tab_immediately()
283    }
284
285    /// Check if the current pane's terminal has a running job that should trigger confirmation
286    ///
287    /// Returns Some(command_name) if confirmation should be shown, None otherwise.
288    pub(super) fn check_current_pane_running_job(&self) -> Option<String> {
289        let tab = self.tab_manager.active_tab()?;
290
291        // If the tab has split panes, check the focused pane
292        if tab.has_multiple_panes() {
293            let pane_manager = tab.pane_manager()?;
294            let focused_id = pane_manager.focused_pane_id()?;
295            let pane = pane_manager.get_pane(focused_id)?;
296            // blocking_read: user-initiated close — must not silently skip confirmation.
297            // should_confirm_close() only needs &self; read lock is correct.
298            let term = pane.terminal.blocking_read();
299            log::info!(
300                "[CLOSE_CONFIRM] check_current_pane_running_job (split pane): marker={:?} command={:?}",
301                term.shell_integration_marker(),
302                term.shell_integration_command()
303            );
304            return term.should_confirm_close(&self.config.load().shell.jobs_to_ignore);
305        }
306
307        // Single pane - use the tab's terminal.
308        // blocking_read: user-initiated close — must not silently skip confirmation.
309        let term = tab.terminal.blocking_read();
310        log::info!(
311            "[CLOSE_CONFIRM] check_current_pane_running_job (single pane): marker={:?} command={:?} is_running={}",
312            term.shell_integration_marker(),
313            term.shell_integration_command(),
314            term.is_command_running()
315        );
316        term.should_confirm_close(&self.config.load().shell.jobs_to_ignore)
317    }
318
319    /// Check if the current tab has multiple panes
320    pub fn has_multiple_panes(&self) -> bool {
321        self.tab_manager
322            .active_tab()
323            .is_some_and(|tab| tab.has_multiple_panes())
324    }
325
326    /// Navigate to an adjacent pane in the given direction
327    pub fn navigate_pane(&mut self, direction: crate::pane::NavigationDirection) {
328        if let Some(tab) = self.tab_manager.active_tab_mut()
329            && tab.has_multiple_panes()
330        {
331            tab.navigate_pane(direction);
332            self.focus_state.needs_redraw = true;
333            self.request_redraw();
334        }
335    }
336
337    /// Resize the focused pane in the given direction
338    ///
339    /// Growing left/up decreases the pane's ratio, growing right/down increases it
340    pub fn resize_pane(&mut self, direction: crate::pane::NavigationDirection) {
341        use crate::pane::NavigationDirection;
342
343        // Resize step: 5% per keypress
344        const RESIZE_DELTA: f32 = 0.05;
345
346        // Determine delta based on direction
347        // Right/Down: grow focused pane (positive delta)
348        // Left/Up: shrink focused pane (negative delta)
349        let delta = match direction {
350            NavigationDirection::Right | NavigationDirection::Down => RESIZE_DELTA,
351            NavigationDirection::Left | NavigationDirection::Up => -RESIZE_DELTA,
352        };
353
354        if let Some(tab) = self.tab_manager.active_tab_mut()
355            && let Some(pm) = tab.pane_manager_mut()
356            && let Some(focused_id) = pm.focused_pane_id()
357        {
358            pm.resize_split(focused_id, delta);
359            self.focus_state.needs_redraw = true;
360            self.request_redraw();
361        }
362    }
363}