Skip to main content

par_term/pane/manager/
creation.rs

1//! Pane creation operations for PaneManager
2//!
3//! Handles creating new panes (initial, split, tmux-driven) and adding
4//! them to the pane tree.
5
6use super::PaneManager;
7use crate::config::Config;
8use crate::pane::tmux_helpers::RemoveResult;
9use crate::pane::types::{Pane, PaneBounds, PaneId, PaneNode, SplitDirection};
10use anyhow::Result;
11use std::sync::Arc;
12use tokio::runtime::Runtime;
13
14impl PaneManager {
15    /// Create the initial pane (when tab is first created)
16    ///
17    /// If bounds have been set on the PaneManager via `set_bounds()`, the pane
18    /// will be created with dimensions calculated from those bounds. Otherwise,
19    /// the default config dimensions are used.
20    pub fn create_initial_pane(
21        &mut self,
22        config: &Config,
23        runtime: Arc<Runtime>,
24        working_directory: Option<String>,
25    ) -> Result<PaneId> {
26        self.create_initial_pane_internal(None, config, runtime, working_directory)
27    }
28
29    /// Create the initial pane sized for an upcoming split
30    ///
31    /// This calculates dimensions based on what the pane size will be AFTER
32    /// the split, preventing the shell from seeing a resize.
33    pub fn create_initial_pane_for_split(
34        &mut self,
35        direction: SplitDirection,
36        config: &Config,
37        runtime: Arc<Runtime>,
38        working_directory: Option<String>,
39    ) -> Result<PaneId> {
40        self.create_initial_pane_internal(Some(direction), config, runtime, working_directory)
41    }
42
43    /// Internal method to create initial pane with optional split direction
44    pub(super) fn create_initial_pane_internal(
45        &mut self,
46        split_direction: Option<SplitDirection>,
47        config: &Config,
48        runtime: Arc<Runtime>,
49        working_directory: Option<String>,
50    ) -> Result<PaneId> {
51        let id = self.next_pane_id;
52        self.next_pane_id += 1;
53
54        // Calculate dimensions from bounds if available
55        let pane_config = if self.total_bounds.width > 0.0 && self.total_bounds.height > 0.0 {
56            // Approximate cell dimensions from font size
57            let cell_width = config.font_size * 0.6; // Approximate monospace char width
58            let cell_height = config.font_size * 1.2; // Approximate line height
59
60            // Calculate bounds accounting for upcoming split
61            let effective_bounds = match split_direction {
62                Some(SplitDirection::Vertical) => {
63                    // After vertical split, this pane will have half the width
64                    PaneBounds::new(
65                        self.total_bounds.x,
66                        self.total_bounds.y,
67                        (self.total_bounds.width - self.divider_width) / 2.0,
68                        self.total_bounds.height,
69                    )
70                }
71                Some(SplitDirection::Horizontal) => {
72                    // After horizontal split, this pane will have half the height
73                    PaneBounds::new(
74                        self.total_bounds.x,
75                        self.total_bounds.y,
76                        self.total_bounds.width,
77                        (self.total_bounds.height - self.divider_width) / 2.0,
78                    )
79                }
80                None => self.total_bounds,
81            };
82
83            let (cols, rows) = effective_bounds.grid_size(cell_width, cell_height);
84
85            let mut cfg = config.clone();
86            cfg.cols = cols.max(10);
87            cfg.rows = rows.max(5);
88            log::info!(
89                "Initial pane {} using bounds-based dimensions: {}x{} (split={:?})",
90                id,
91                cfg.cols,
92                cfg.rows,
93                split_direction
94            );
95            cfg
96        } else {
97            log::info!(
98                "Initial pane {} using config dimensions: {}x{}",
99                id,
100                config.cols,
101                config.rows
102            );
103            config.clone()
104        };
105
106        let mut pane = Pane::new(id, &pane_config, runtime, working_directory)?;
107
108        // Apply per-pane background from config if available (index 0 for initial pane)
109        if let Some((image_path, mode, opacity, darken)) = config.get_pane_background(0) {
110            pane.set_background(crate::pane::PaneBackground {
111                image_path: Some(image_path),
112                mode,
113                opacity,
114                darken,
115            });
116        }
117
118        self.root = Some(PaneNode::leaf(pane));
119        self.focused_pane_id = Some(id);
120
121        Ok(id)
122    }
123
124    /// Split the focused pane in the given direction
125    ///
126    /// Returns the ID of the new pane, or None if no pane is focused.
127    ///
128    /// `initial_command` — when `Some((cmd, args))` the new pane launches that
129    /// process directly instead of the login shell. The pane closes when the
130    /// process exits.
131    pub fn split(
132        &mut self,
133        direction: SplitDirection,
134        focus_new: bool,
135        config: &Config,
136        runtime: Arc<Runtime>,
137        initial_command: Option<(String, Vec<String>)>,
138        ratio: f32,
139    ) -> Result<Option<PaneId>> {
140        let focused_id = match self.focused_pane_id {
141            Some(id) => id,
142            None => return Ok(None),
143        };
144
145        // Get the working directory and bounds from the focused pane
146        let (working_dir, focused_bounds) = if let Some(pane) = self.focused_pane() {
147            (pane.get_cwd(), pane.bounds)
148        } else {
149            (None, self.total_bounds)
150        };
151
152        // Calculate approximate dimensions for the new pane (half of focused pane)
153        let (new_cols, new_rows) = match direction {
154            SplitDirection::Vertical => {
155                // New pane gets half the width
156                let half_width = (focused_bounds.width - self.divider_width) / 2.0;
157                let cols = (half_width / config.font_size * 1.8).floor() as usize; // Approximate
158                (cols.max(10), config.rows)
159            }
160            SplitDirection::Horizontal => {
161                // New pane gets half the height
162                let half_height = (focused_bounds.height - self.divider_width) / 2.0;
163                let rows = (half_height / (config.font_size * 1.2)).floor() as usize; // Approximate
164                (config.cols, rows.max(5))
165            }
166        };
167
168        // Create a modified config with the approximate dimensions
169        let mut pane_config = config.clone();
170        pane_config.cols = new_cols;
171        pane_config.rows = new_rows;
172
173        // Create the new pane with approximate dimensions
174        let new_id = self.next_pane_id;
175        self.next_pane_id += 1;
176
177        let mut new_pane = if let Some((cmd, args)) = initial_command {
178            Pane::new_with_command(new_id, &pane_config, runtime, working_dir, cmd, args)?
179        } else {
180            Pane::new(new_id, &pane_config, runtime, working_dir)?
181        };
182
183        // Apply per-pane background from config if available
184        // The new pane will be at the end of the pane list, so its index is the current count
185        let new_pane_index = self.pane_count(); // current count = index of new pane after insertion
186        if let Some((image_path, mode, opacity, darken)) =
187            config.get_pane_background(new_pane_index)
188        {
189            new_pane.set_background(crate::pane::PaneBackground {
190                image_path: Some(image_path),
191                mode,
192                opacity,
193                darken,
194            });
195        }
196
197        // Find and split the focused pane
198        if let Some(root) = self.root.take() {
199            let (new_root, _) =
200                Self::split_node(root, focused_id, direction, Some(new_pane), ratio);
201            self.root = Some(new_root);
202        }
203
204        // Recalculate bounds
205        self.recalculate_bounds();
206
207        // Focus the new pane only if requested
208        if focus_new {
209            self.focused_pane_id = Some(new_id);
210        }
211
212        crate::debug_info!(
213            "PANE_SPLIT",
214            "Split pane {} {:?}, created new pane {}. First(left/top)={} Second(right/bottom)={} (focused)",
215            focused_id,
216            direction,
217            new_id,
218            focused_id,
219            new_id
220        );
221
222        Ok(Some(new_id))
223    }
224
225    /// Split a node, finding the target pane and replacing it with a split
226    ///
227    /// Returns (new_node, remaining_pane) where remaining_pane is Some if
228    /// the target was not found in this subtree.
229    pub(super) fn split_node(
230        node: PaneNode,
231        target_id: PaneId,
232        direction: SplitDirection,
233        new_pane: Option<Pane>,
234        ratio: f32,
235    ) -> (PaneNode, Option<Pane>) {
236        match node {
237            PaneNode::Leaf(pane) => {
238                if pane.id == target_id {
239                    if let Some(new) = new_pane {
240                        // This is the pane to split - create a new split node
241                        (
242                            PaneNode::split(
243                                direction,
244                                ratio,
245                                PaneNode::leaf(*pane),
246                                PaneNode::leaf(new),
247                            ),
248                            None,
249                        )
250                    } else {
251                        // No pane to insert (shouldn't happen)
252                        (PaneNode::Leaf(pane), None)
253                    }
254                } else {
255                    // Not the target, keep as-is and pass the new_pane through
256                    (PaneNode::Leaf(pane), new_pane)
257                }
258            }
259            PaneNode::Split {
260                direction: split_dir,
261                ratio: existing_ratio,
262                first,
263                second,
264            } => {
265                // Try to insert in first child
266                let (new_first, remaining) =
267                    Self::split_node(*first, target_id, direction, new_pane, ratio);
268
269                if remaining.is_none() {
270                    // Target was found in first child
271                    (
272                        PaneNode::Split {
273                            direction: split_dir,
274                            ratio: existing_ratio,
275                            first: Box::new(new_first),
276                            second,
277                        },
278                        None,
279                    )
280                } else {
281                    // Target not in first, try second
282                    let (new_second, remaining) =
283                        Self::split_node(*second, target_id, direction, remaining, ratio);
284                    (
285                        PaneNode::Split {
286                            direction: split_dir,
287                            ratio: existing_ratio,
288                            first: Box::new(new_first),
289                            second: Box::new(new_second),
290                        },
291                        remaining,
292                    )
293                }
294            }
295        }
296    }
297
298    /// Remove a pane from the tree, returning the new tree structure
299    pub(super) fn remove_pane(node: PaneNode, target_id: PaneId) -> RemoveResult {
300        match node {
301            PaneNode::Leaf(pane) => {
302                if pane.id == target_id {
303                    // This pane should be removed
304                    RemoveResult::Removed(None)
305                } else {
306                    RemoveResult::NotFound(PaneNode::Leaf(pane))
307                }
308            }
309            PaneNode::Split {
310                direction,
311                ratio,
312                first,
313                second,
314            } => {
315                // Try to remove from first child
316                match Self::remove_pane(*first, target_id) {
317                    RemoveResult::Removed(None) => {
318                        // First child was the target and is now gone
319                        // Replace this split with the second child
320                        RemoveResult::Removed(Some(*second))
321                    }
322                    RemoveResult::Removed(Some(new_first)) => {
323                        // First child was modified
324                        RemoveResult::Removed(Some(PaneNode::Split {
325                            direction,
326                            ratio,
327                            first: Box::new(new_first),
328                            second,
329                        }))
330                    }
331                    RemoveResult::NotFound(first_node) => {
332                        // Target not in first child, try second
333                        match Self::remove_pane(*second, target_id) {
334                            RemoveResult::Removed(None) => {
335                                // Second child was the target and is now gone
336                                // Replace this split with the first child
337                                RemoveResult::Removed(Some(first_node))
338                            }
339                            RemoveResult::Removed(Some(new_second)) => {
340                                // Second child was modified
341                                RemoveResult::Removed(Some(PaneNode::Split {
342                                    direction,
343                                    ratio,
344                                    first: Box::new(first_node),
345                                    second: Box::new(new_second),
346                                }))
347                            }
348                            RemoveResult::NotFound(second_node) => {
349                                // Target not found in either child
350                                RemoveResult::NotFound(PaneNode::Split {
351                                    direction,
352                                    ratio,
353                                    first: Box::new(first_node),
354                                    second: Box::new(second_node),
355                                })
356                            }
357                        }
358                    }
359                }
360            }
361        }
362    }
363
364    /// Recursive helper: find a target leaf and replace it with a Split
365    /// containing the leaf and the subtree to insert.
366    ///
367    /// Returns `Ok(new_node)` on success, or `Err(original_node)` if the
368    /// target was not found (so the caller can restore the tree).
369    pub(super) fn insert_subtree_at_node(
370        node: PaneNode,
371        target_id: PaneId,
372        subtree: PaneNode,
373        direction: crate::pane::types::SplitDirection,
374        ratio: f32,
375    ) -> Result<PaneNode, (PaneNode, PaneNode)> {
376        match node {
377            PaneNode::Leaf(pane) => {
378                if pane.id == target_id {
379                    Ok(PaneNode::Split {
380                        direction,
381                        ratio,
382                        first: Box::new(PaneNode::Leaf(pane)),
383                        second: Box::new(subtree),
384                    })
385                } else {
386                    Err((PaneNode::Leaf(pane), subtree))
387                }
388            }
389            PaneNode::Split {
390                direction: split_dir,
391                ratio: existing_ratio,
392                first,
393                second,
394            } => match Self::insert_subtree_at_node(*first, target_id, subtree, direction, ratio) {
395                Ok(new_first) => Ok(PaneNode::Split {
396                    direction: split_dir,
397                    ratio: existing_ratio,
398                    first: Box::new(new_first),
399                    second,
400                }),
401                Err((first_node, subtree)) => {
402                    match Self::insert_subtree_at_node(
403                        *second, target_id, subtree, direction, ratio,
404                    ) {
405                        Ok(new_second) => Ok(PaneNode::Split {
406                            direction: split_dir,
407                            ratio: existing_ratio,
408                            first: Box::new(first_node),
409                            second: Box::new(new_second),
410                        }),
411                        Err((second_node, subtree)) => Err((
412                            PaneNode::Split {
413                                direction: split_dir,
414                                ratio: existing_ratio,
415                                first: Box::new(first_node),
416                                second: Box::new(second_node),
417                            },
418                            subtree,
419                        )),
420                    }
421                }
422            },
423        }
424    }
425
426    /// Recursive helper: extract a pane from the tree, returning the live Pane.
427    pub(super) fn extract_pane_from_node(
428        node: PaneNode,
429        target_id: PaneId,
430    ) -> super::ExtractInternal {
431        match node {
432            PaneNode::Leaf(pane) => {
433                if pane.id == target_id {
434                    super::ExtractInternal::OnlyPane(*pane)
435                } else {
436                    super::ExtractInternal::NotFound(PaneNode::Leaf(pane))
437                }
438            }
439            PaneNode::Split {
440                direction,
441                ratio,
442                first,
443                second,
444            } => match Self::extract_pane_from_node(*first, target_id) {
445                super::ExtractInternal::OnlyPane(pane) => super::ExtractInternal::Extracted {
446                    pane,
447                    remaining: *second,
448                },
449                super::ExtractInternal::Extracted { pane, remaining } => {
450                    super::ExtractInternal::Extracted {
451                        pane,
452                        remaining: PaneNode::Split {
453                            direction,
454                            ratio,
455                            first: Box::new(remaining),
456                            second,
457                        },
458                    }
459                }
460                super::ExtractInternal::NotFound(first_node) => {
461                    match Self::extract_pane_from_node(*second, target_id) {
462                        super::ExtractInternal::OnlyPane(pane) => {
463                            super::ExtractInternal::Extracted {
464                                pane,
465                                remaining: first_node,
466                            }
467                        }
468                        super::ExtractInternal::Extracted { pane, remaining } => {
469                            super::ExtractInternal::Extracted {
470                                pane,
471                                remaining: PaneNode::Split {
472                                    direction,
473                                    ratio,
474                                    first: Box::new(first_node),
475                                    second: Box::new(remaining),
476                                },
477                            }
478                        }
479                        super::ExtractInternal::NotFound(second_node) => {
480                            super::ExtractInternal::NotFound(PaneNode::Split {
481                                direction,
482                                ratio,
483                                first: Box::new(first_node),
484                                second: Box::new(second_node),
485                            })
486                        }
487                    }
488                }
489            },
490        }
491    }
492}