Skip to main content

par_term/pane/
manager.rs

1//! Pane manager for coordinating pane operations within a tab
2//!
3//! The PaneManager owns the pane tree and provides operations for:
4//! - Splitting panes horizontally and vertically
5//! - Closing panes
6//! - Navigating between panes
7//! - Resizing panes
8
9use super::types::{
10    DividerRect, NavigationDirection, Pane, PaneBounds, PaneId, PaneNode, SplitDirection,
11};
12use crate::config::{Config, PaneBackgroundConfig};
13use crate::session::SessionPaneNode;
14use crate::tmux::{LayoutNode, TmuxLayout, TmuxPaneId};
15use anyhow::Result;
16use std::collections::HashMap;
17use std::sync::Arc;
18use tokio::runtime::Runtime;
19
20/// Manages the pane tree within a single tab
21pub struct PaneManager {
22    /// Root of the pane tree (None if no panes yet)
23    root: Option<PaneNode>,
24    /// ID of the currently focused pane
25    focused_pane_id: Option<PaneId>,
26    /// Counter for generating unique pane IDs
27    next_pane_id: PaneId,
28    /// Width of dividers between panes in pixels
29    divider_width: f32,
30    /// Width of the hit area for divider drag detection
31    divider_hit_width: f32,
32    /// Current total bounds available for panes
33    total_bounds: PaneBounds,
34}
35
36impl PaneManager {
37    /// Create a new empty pane manager
38    pub fn new() -> Self {
39        Self {
40            root: None,
41            focused_pane_id: None,
42            next_pane_id: 1,
43            divider_width: 1.0,     // Default 1 pixel divider
44            divider_hit_width: 8.0, // Default 8 pixel hit area
45            total_bounds: PaneBounds::default(),
46        }
47    }
48
49    /// Create a pane manager with an initial pane
50    pub fn with_initial_pane(
51        config: &Config,
52        runtime: Arc<Runtime>,
53        working_directory: Option<String>,
54    ) -> Result<Self> {
55        let mut manager = Self::new();
56        manager.divider_width = config.pane_divider_width.unwrap_or(1.0);
57        manager.divider_hit_width = config.pane_divider_hit_width;
58        manager.create_initial_pane(config, runtime, working_directory)?;
59        Ok(manager)
60    }
61
62    /// Create the initial pane (when tab is first created)
63    ///
64    /// If bounds have been set on the PaneManager via `set_bounds()`, the pane
65    /// will be created with dimensions calculated from those bounds. Otherwise,
66    /// the default config dimensions are used.
67    pub fn create_initial_pane(
68        &mut self,
69        config: &Config,
70        runtime: Arc<Runtime>,
71        working_directory: Option<String>,
72    ) -> Result<PaneId> {
73        self.create_initial_pane_internal(None, config, runtime, working_directory)
74    }
75
76    /// Create the initial pane sized for an upcoming split
77    ///
78    /// This calculates dimensions based on what the pane size will be AFTER
79    /// the split, preventing the shell from seeing a resize.
80    pub fn create_initial_pane_for_split(
81        &mut self,
82        direction: SplitDirection,
83        config: &Config,
84        runtime: Arc<Runtime>,
85        working_directory: Option<String>,
86    ) -> Result<PaneId> {
87        self.create_initial_pane_internal(Some(direction), config, runtime, working_directory)
88    }
89
90    /// Internal method to create initial pane with optional split direction
91    fn create_initial_pane_internal(
92        &mut self,
93        split_direction: Option<SplitDirection>,
94        config: &Config,
95        runtime: Arc<Runtime>,
96        working_directory: Option<String>,
97    ) -> Result<PaneId> {
98        let id = self.next_pane_id;
99        self.next_pane_id += 1;
100
101        // Calculate dimensions from bounds if available
102        let pane_config = if self.total_bounds.width > 0.0 && self.total_bounds.height > 0.0 {
103            // Approximate cell dimensions from font size
104            let cell_width = config.font_size * 0.6; // Approximate monospace char width
105            let cell_height = config.font_size * 1.2; // Approximate line height
106
107            // Calculate bounds accounting for upcoming split
108            let effective_bounds = match split_direction {
109                Some(SplitDirection::Vertical) => {
110                    // After vertical split, this pane will have half the width
111                    PaneBounds::new(
112                        self.total_bounds.x,
113                        self.total_bounds.y,
114                        (self.total_bounds.width - self.divider_width) / 2.0,
115                        self.total_bounds.height,
116                    )
117                }
118                Some(SplitDirection::Horizontal) => {
119                    // After horizontal split, this pane will have half the height
120                    PaneBounds::new(
121                        self.total_bounds.x,
122                        self.total_bounds.y,
123                        self.total_bounds.width,
124                        (self.total_bounds.height - self.divider_width) / 2.0,
125                    )
126                }
127                None => self.total_bounds,
128            };
129
130            let (cols, rows) = effective_bounds.grid_size(cell_width, cell_height);
131
132            let mut cfg = config.clone();
133            cfg.cols = cols.max(10);
134            cfg.rows = rows.max(5);
135            log::info!(
136                "Initial pane {} using bounds-based dimensions: {}x{} (split={:?})",
137                id,
138                cfg.cols,
139                cfg.rows,
140                split_direction
141            );
142            cfg
143        } else {
144            log::info!(
145                "Initial pane {} using config dimensions: {}x{}",
146                id,
147                config.cols,
148                config.rows
149            );
150            config.clone()
151        };
152
153        let mut pane = Pane::new(id, &pane_config, runtime, working_directory)?;
154
155        // Apply per-pane background from config if available (index 0 for initial pane)
156        if let Some((image_path, mode, opacity)) = config.get_pane_background(0) {
157            pane.set_background(crate::pane::PaneBackground {
158                image_path: Some(image_path),
159                mode,
160                opacity,
161            });
162        }
163
164        self.root = Some(PaneNode::leaf(pane));
165        self.focused_pane_id = Some(id);
166
167        Ok(id)
168    }
169
170    /// Split the focused pane in the given direction
171    ///
172    /// Returns the ID of the new pane, or None if no pane is focused
173    pub fn split(
174        &mut self,
175        direction: SplitDirection,
176        config: &Config,
177        runtime: Arc<Runtime>,
178    ) -> Result<Option<PaneId>> {
179        let focused_id = match self.focused_pane_id {
180            Some(id) => id,
181            None => return Ok(None),
182        };
183
184        // Get the working directory and bounds from the focused pane
185        let (working_dir, focused_bounds) = if let Some(pane) = self.focused_pane() {
186            (pane.get_cwd(), pane.bounds)
187        } else {
188            (None, self.total_bounds)
189        };
190
191        // Calculate approximate dimensions for the new pane (half of focused pane)
192        let (new_cols, new_rows) = match direction {
193            SplitDirection::Vertical => {
194                // New pane gets half the width
195                let half_width = (focused_bounds.width - self.divider_width) / 2.0;
196                let cols = (half_width / config.font_size * 1.8).floor() as usize; // Approximate
197                (cols.max(10), config.rows)
198            }
199            SplitDirection::Horizontal => {
200                // New pane gets half the height
201                let half_height = (focused_bounds.height - self.divider_width) / 2.0;
202                let rows = (half_height / (config.font_size * 1.2)).floor() as usize; // Approximate
203                (config.cols, rows.max(5))
204            }
205        };
206
207        // Create a modified config with the approximate dimensions
208        let mut pane_config = config.clone();
209        pane_config.cols = new_cols;
210        pane_config.rows = new_rows;
211
212        // Create the new pane with approximate dimensions
213        let new_id = self.next_pane_id;
214        self.next_pane_id += 1;
215
216        let mut new_pane = Pane::new(new_id, &pane_config, runtime, working_dir)?;
217
218        // Apply per-pane background from config if available
219        // The new pane will be at the end of the pane list, so its index is the current count
220        let new_pane_index = self.pane_count(); // current count = index of new pane after insertion
221        if let Some((image_path, mode, opacity)) = config.get_pane_background(new_pane_index) {
222            new_pane.set_background(crate::pane::PaneBackground {
223                image_path: Some(image_path),
224                mode,
225                opacity,
226            });
227        }
228
229        // Find and split the focused pane
230        if let Some(root) = self.root.take() {
231            let (new_root, _) = Self::split_node(root, focused_id, direction, Some(new_pane));
232            self.root = Some(new_root);
233        }
234
235        // Recalculate bounds
236        self.recalculate_bounds();
237
238        // Focus the new pane
239        self.focused_pane_id = Some(new_id);
240
241        crate::debug_info!(
242            "PANE_SPLIT",
243            "Split pane {} {:?}, created new pane {}. First(left/top)={} Second(right/bottom)={} (focused)",
244            focused_id,
245            direction,
246            new_id,
247            focused_id,
248            new_id
249        );
250
251        Ok(Some(new_id))
252    }
253
254    /// Split a node, finding the target pane and replacing it with a split
255    ///
256    /// Returns (new_node, remaining_pane) where remaining_pane is Some if
257    /// the target was not found in this subtree.
258    fn split_node(
259        node: PaneNode,
260        target_id: PaneId,
261        direction: SplitDirection,
262        new_pane: Option<Pane>,
263    ) -> (PaneNode, Option<Pane>) {
264        match node {
265            PaneNode::Leaf(pane) => {
266                if pane.id == target_id {
267                    if let Some(new) = new_pane {
268                        // This is the pane to split - create a new split node
269                        (
270                            PaneNode::split(
271                                direction,
272                                0.5, // 50/50 split
273                                PaneNode::leaf(*pane),
274                                PaneNode::leaf(new),
275                            ),
276                            None,
277                        )
278                    } else {
279                        // No pane to insert (shouldn't happen)
280                        (PaneNode::Leaf(pane), None)
281                    }
282                } else {
283                    // Not the target, keep as-is and pass the new_pane through
284                    (PaneNode::Leaf(pane), new_pane)
285                }
286            }
287            PaneNode::Split {
288                direction: split_dir,
289                ratio,
290                first,
291                second,
292            } => {
293                // Try to insert in first child
294                let (new_first, remaining) =
295                    Self::split_node(*first, target_id, direction, new_pane);
296
297                if remaining.is_none() {
298                    // Target was found in first child
299                    (
300                        PaneNode::Split {
301                            direction: split_dir,
302                            ratio,
303                            first: Box::new(new_first),
304                            second,
305                        },
306                        None,
307                    )
308                } else {
309                    // Target not in first, try second
310                    let (new_second, remaining) =
311                        Self::split_node(*second, target_id, direction, remaining);
312                    (
313                        PaneNode::Split {
314                            direction: split_dir,
315                            ratio,
316                            first: Box::new(new_first),
317                            second: Box::new(new_second),
318                        },
319                        remaining,
320                    )
321                }
322            }
323        }
324    }
325
326    /// Close a pane by ID
327    ///
328    /// Returns true if this was the last pane (tab should close)
329    pub fn close_pane(&mut self, id: PaneId) -> bool {
330        crate::debug_info!("PANE_CLOSE", "close_pane called for pane {}", id);
331
332        if let Some(root) = self.root.take() {
333            match Self::remove_pane(root, id) {
334                RemoveResult::Removed(new_root) => {
335                    self.root = new_root;
336
337                    // If we closed the focused pane, focus another
338                    if self.focused_pane_id == Some(id) {
339                        let new_focus = self
340                            .root
341                            .as_ref()
342                            .and_then(|r| r.all_pane_ids().first().copied());
343                        crate::debug_info!(
344                            "PANE_CLOSE",
345                            "Closed focused pane {}, new focus: {:?}",
346                            id,
347                            new_focus
348                        );
349                        self.focused_pane_id = new_focus;
350                    }
351
352                    // Recalculate bounds
353                    self.recalculate_bounds();
354
355                    // Log remaining panes after closure
356                    if let Some(ref root) = self.root {
357                        for pane_id in root.all_pane_ids() {
358                            if let Some(pane) = self.get_pane(pane_id) {
359                                crate::debug_info!(
360                                    "PANE_CLOSE",
361                                    "Remaining pane {} bounds=({:.0},{:.0} {:.0}x{:.0})",
362                                    pane.id,
363                                    pane.bounds.x,
364                                    pane.bounds.y,
365                                    pane.bounds.width,
366                                    pane.bounds.height
367                                );
368                            }
369                        }
370                    }
371
372                    crate::debug_info!("PANE_CLOSE", "Successfully closed pane {}", id);
373                }
374                RemoveResult::NotFound(root) => {
375                    crate::debug_info!("PANE_CLOSE", "Pane {} not found in tree", id);
376                    self.root = Some(root);
377                }
378            }
379        }
380
381        self.root.is_none()
382    }
383
384    /// Remove a pane from the tree, returning the new tree structure
385    fn remove_pane(node: PaneNode, target_id: PaneId) -> RemoveResult {
386        match node {
387            PaneNode::Leaf(pane) => {
388                if pane.id == target_id {
389                    // This pane should be removed
390                    RemoveResult::Removed(None)
391                } else {
392                    RemoveResult::NotFound(PaneNode::Leaf(pane))
393                }
394            }
395            PaneNode::Split {
396                direction,
397                ratio,
398                first,
399                second,
400            } => {
401                // Try to remove from first child
402                match Self::remove_pane(*first, target_id) {
403                    RemoveResult::Removed(None) => {
404                        // First child was the target and is now gone
405                        // Replace this split with the second child
406                        RemoveResult::Removed(Some(*second))
407                    }
408                    RemoveResult::Removed(Some(new_first)) => {
409                        // First child was modified
410                        RemoveResult::Removed(Some(PaneNode::Split {
411                            direction,
412                            ratio,
413                            first: Box::new(new_first),
414                            second,
415                        }))
416                    }
417                    RemoveResult::NotFound(first_node) => {
418                        // Target not in first child, try second
419                        match Self::remove_pane(*second, target_id) {
420                            RemoveResult::Removed(None) => {
421                                // Second child was the target and is now gone
422                                // Replace this split with the first child
423                                RemoveResult::Removed(Some(first_node))
424                            }
425                            RemoveResult::Removed(Some(new_second)) => {
426                                // Second child was modified
427                                RemoveResult::Removed(Some(PaneNode::Split {
428                                    direction,
429                                    ratio,
430                                    first: Box::new(first_node),
431                                    second: Box::new(new_second),
432                                }))
433                            }
434                            RemoveResult::NotFound(second_node) => {
435                                // Target not found in either child
436                                RemoveResult::NotFound(PaneNode::Split {
437                                    direction,
438                                    ratio,
439                                    first: Box::new(first_node),
440                                    second: Box::new(second_node),
441                                })
442                            }
443                        }
444                    }
445                }
446            }
447        }
448    }
449
450    /// Navigate to a pane in the given direction
451    pub fn navigate(&mut self, direction: NavigationDirection) {
452        if let Some(focused_id) = self.focused_pane_id
453            && let Some(ref root) = self.root
454            && let Some(new_id) = root.find_pane_in_direction(focused_id, direction)
455        {
456            self.focused_pane_id = Some(new_id);
457            log::debug!(
458                "Navigated {:?} from pane {} to pane {}",
459                direction,
460                focused_id,
461                new_id
462            );
463        }
464    }
465
466    /// Focus a specific pane by ID
467    pub fn focus_pane(&mut self, id: PaneId) {
468        if self
469            .root
470            .as_ref()
471            .is_some_and(|r| r.find_pane(id).is_some())
472        {
473            self.focused_pane_id = Some(id);
474        }
475    }
476
477    /// Focus the pane at a given pixel position
478    pub fn focus_pane_at(&mut self, x: f32, y: f32) -> Option<PaneId> {
479        if let Some(ref root) = self.root
480            && let Some(pane) = root.find_pane_at(x, y)
481        {
482            let id = pane.id;
483            self.focused_pane_id = Some(id);
484            return Some(id);
485        }
486        None
487    }
488
489    /// Get the currently focused pane
490    pub fn focused_pane(&self) -> Option<&Pane> {
491        self.focused_pane_id
492            .and_then(|id| self.root.as_ref()?.find_pane(id))
493    }
494
495    /// Get the currently focused pane mutably
496    pub fn focused_pane_mut(&mut self) -> Option<&mut Pane> {
497        let id = self.focused_pane_id?;
498        self.root.as_mut()?.find_pane_mut(id)
499    }
500
501    /// Get the focused pane ID
502    pub fn focused_pane_id(&self) -> Option<PaneId> {
503        self.focused_pane_id
504    }
505
506    /// Get the next pane ID that will be assigned
507    pub fn next_pane_id(&self) -> PaneId {
508        self.next_pane_id
509    }
510
511    /// Add a pane for tmux integration (doesn't create split, just adds to flat structure)
512    ///
513    /// This is used when tmux splits a pane - we need to add a new native pane
514    /// without restructuring our tree (tmux layout update will handle that).
515    pub fn add_pane_for_tmux(&mut self, pane: Pane) {
516        let pane_id = pane.id;
517
518        // Update next_pane_id if needed
519        if pane_id >= self.next_pane_id {
520            self.next_pane_id = pane_id + 1;
521        }
522
523        // If no root, this becomes the root
524        if self.root.is_none() {
525            self.root = Some(PaneNode::leaf(pane));
526            self.focused_pane_id = Some(pane_id);
527            return;
528        }
529
530        // Otherwise, we need to add it to the tree structure
531        // For now, we'll create a simple vertical split with the new pane
532        // The actual layout will be corrected by update_layout_from_tmux
533        if let Some(existing_root) = self.root.take() {
534            self.root = Some(PaneNode::Split {
535                direction: SplitDirection::Vertical,
536                ratio: 0.5,
537                first: Box::new(existing_root),
538                second: Box::new(PaneNode::leaf(pane)),
539            });
540        }
541
542        // Focus the new pane
543        self.focused_pane_id = Some(pane_id);
544    }
545
546    /// Get a pane by ID
547    pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
548        self.root.as_ref()?.find_pane(id)
549    }
550
551    /// Get a mutable pane by ID
552    pub fn get_pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
553        self.root.as_mut()?.find_pane_mut(id)
554    }
555
556    /// Get all panes
557    pub fn all_panes(&self) -> Vec<&Pane> {
558        self.root
559            .as_ref()
560            .map(|r| r.all_panes())
561            .unwrap_or_default()
562    }
563
564    /// Get all panes mutably
565    pub fn all_panes_mut(&mut self) -> Vec<&mut Pane> {
566        self.root
567            .as_mut()
568            .map(|r| r.all_panes_mut())
569            .unwrap_or_default()
570    }
571
572    /// Collect current per-pane background settings for config persistence
573    ///
574    /// Returns a `Vec<PaneBackgroundConfig>` containing only panes that have
575    /// a custom background image set. The `index` field corresponds to the
576    /// pane's position in the tree traversal order.
577    pub fn collect_pane_backgrounds(&self) -> Vec<PaneBackgroundConfig> {
578        self.all_panes()
579            .iter()
580            .enumerate()
581            .filter_map(|(index, pane)| {
582                pane.background
583                    .image_path
584                    .as_ref()
585                    .map(|path| PaneBackgroundConfig {
586                        index,
587                        image: path.clone(),
588                        mode: pane.background.mode,
589                        opacity: pane.background.opacity,
590                    })
591            })
592            .collect()
593    }
594
595    /// Get the number of panes
596    pub fn pane_count(&self) -> usize {
597        self.root.as_ref().map(|r| r.pane_count()).unwrap_or(0)
598    }
599
600    /// Check if there are multiple panes
601    pub fn has_multiple_panes(&self) -> bool {
602        self.pane_count() > 1
603    }
604
605    /// Set the total bounds available for panes and recalculate layout
606    pub fn set_bounds(&mut self, bounds: PaneBounds) {
607        self.total_bounds = bounds;
608        self.recalculate_bounds();
609    }
610
611    /// Recalculate bounds for all panes
612    pub fn recalculate_bounds(&mut self) {
613        if let Some(ref mut root) = self.root {
614            root.calculate_bounds(self.total_bounds, self.divider_width);
615        }
616    }
617
618    /// Resize all pane terminals to match their current bounds
619    ///
620    /// This should be called after bounds are updated (split, resize, window resize)
621    /// to ensure each PTY is sized correctly for its pane area.
622    pub fn resize_all_terminals(&self, cell_width: f32, cell_height: f32) {
623        self.resize_all_terminals_with_padding(cell_width, cell_height, 0.0, 0.0);
624    }
625
626    /// Resize all terminal PTYs to match their pane bounds, accounting for padding.
627    ///
628    /// The padding reduces the content area where text is rendered, so terminals
629    /// should be sized for the padded (smaller) area to avoid content being cut off.
630    ///
631    /// `height_offset` is an additional height reduction (e.g., pane title bar height)
632    /// subtracted once from each pane's content height.
633    pub fn resize_all_terminals_with_padding(
634        &self,
635        cell_width: f32,
636        cell_height: f32,
637        padding: f32,
638        height_offset: f32,
639    ) {
640        if let Some(ref root) = self.root {
641            for pane in root.all_panes() {
642                // Calculate content size (bounds minus padding on each side, minus title bar)
643                let content_width = (pane.bounds.width - padding * 2.0).max(cell_width);
644                let content_height =
645                    (pane.bounds.height - padding * 2.0 - height_offset).max(cell_height);
646
647                let cols = (content_width / cell_width).floor() as usize;
648                let rows = (content_height / cell_height).floor() as usize;
649
650                pane.resize_terminal(cols.max(1), rows.max(1));
651            }
652        }
653    }
654
655    /// Set the divider width
656    pub fn set_divider_width(&mut self, width: f32) {
657        self.divider_width = width;
658        self.recalculate_bounds();
659    }
660
661    /// Get the divider width
662    pub fn divider_width(&self) -> f32 {
663        self.divider_width
664    }
665
666    /// Get the hit detection padding (extra area around divider for easier grabbing)
667    pub fn divider_hit_padding(&self) -> f32 {
668        (self.divider_hit_width - self.divider_width).max(0.0) / 2.0
669    }
670
671    /// Resize a split by adjusting its ratio
672    ///
673    /// `pane_id`: The pane whose adjacent split should be resized
674    /// `delta`: Amount to adjust the ratio (-1.0 to 1.0)
675    pub fn resize_split(&mut self, pane_id: PaneId, delta: f32) {
676        if let Some(ref mut root) = self.root {
677            Self::adjust_split_ratio(root, pane_id, delta);
678            self.recalculate_bounds();
679        }
680    }
681
682    /// Recursively find and adjust the split ratio for a pane
683    fn adjust_split_ratio(node: &mut PaneNode, target_id: PaneId, delta: f32) -> bool {
684        match node {
685            PaneNode::Leaf(_) => false,
686            PaneNode::Split {
687                ratio,
688                first,
689                second,
690                ..
691            } => {
692                // Check if target is in first child
693                if first.all_pane_ids().contains(&target_id) {
694                    // Try to find in nested splits first
695                    if Self::adjust_split_ratio(first, target_id, delta) {
696                        return true;
697                    }
698                    // Adjust this split's ratio (making first child larger/smaller)
699                    *ratio = (*ratio + delta).clamp(0.1, 0.9);
700                    return true;
701                }
702
703                // Check if target is in second child
704                if second.all_pane_ids().contains(&target_id) {
705                    // Try to find in nested splits first
706                    if Self::adjust_split_ratio(second, target_id, delta) {
707                        return true;
708                    }
709                    // Adjust this split's ratio (making second child larger/smaller)
710                    *ratio = (*ratio - delta).clamp(0.1, 0.9);
711                    return true;
712                }
713
714                false
715            }
716        }
717    }
718
719    /// Get access to the root node (for rendering)
720    pub fn root(&self) -> Option<&PaneNode> {
721        self.root.as_ref()
722    }
723
724    /// Get mutable access to the root node
725    pub fn root_mut(&mut self) -> Option<&mut PaneNode> {
726        self.root.as_mut()
727    }
728
729    /// Get all divider rectangles in the pane tree
730    pub fn get_dividers(&self) -> Vec<DividerRect> {
731        self.root
732            .as_ref()
733            .map(|r| r.collect_dividers(self.total_bounds, self.divider_width))
734            .unwrap_or_default()
735    }
736
737    /// Find a divider at the given position
738    ///
739    /// Returns the index of the divider if found, with optional padding for easier grabbing
740    pub fn find_divider_at(&self, x: f32, y: f32, padding: f32) -> Option<usize> {
741        let dividers = self.get_dividers();
742        for (i, divider) in dividers.iter().enumerate() {
743            if divider.contains(x, y, padding) {
744                return Some(i);
745            }
746        }
747        None
748    }
749
750    /// Check if a position is on a divider
751    pub fn is_on_divider(&self, x: f32, y: f32) -> bool {
752        let padding = (self.divider_hit_width - self.divider_width).max(0.0) / 2.0;
753        self.find_divider_at(x, y, padding).is_some()
754    }
755
756    /// Set the divider hit width
757    pub fn set_divider_hit_width(&mut self, width: f32) {
758        self.divider_hit_width = width;
759    }
760
761    /// Get the divider at an index
762    pub fn get_divider(&self, index: usize) -> Option<DividerRect> {
763        self.get_dividers().get(index).copied()
764    }
765
766    /// Resize by dragging a divider to a new position
767    ///
768    /// `divider_index`: Which divider is being dragged
769    /// `new_position`: New mouse position (x for vertical, y for horizontal dividers)
770    pub fn drag_divider(&mut self, divider_index: usize, new_x: f32, new_y: f32) {
771        // Get the divider info first
772        let dividers = self.get_dividers();
773        if let Some(divider) = dividers.get(divider_index) {
774            // Find the split node that owns this divider and update its ratio
775            if let Some(ref mut root) = self.root {
776                let mut divider_count = 0;
777                Self::update_divider_ratio(
778                    root,
779                    divider_index,
780                    &mut divider_count,
781                    divider.is_horizontal,
782                    new_x,
783                    new_y,
784                    self.total_bounds,
785                    self.divider_width,
786                );
787                self.recalculate_bounds();
788            }
789        }
790    }
791
792    /// Recursively find and update the split ratio for a divider
793    #[allow(clippy::only_used_in_recursion, clippy::too_many_arguments)]
794    fn update_divider_ratio(
795        node: &mut PaneNode,
796        target_index: usize,
797        current_index: &mut usize,
798        is_horizontal: bool,
799        new_x: f32,
800        new_y: f32,
801        bounds: PaneBounds,
802        divider_width: f32,
803    ) -> bool {
804        match node {
805            PaneNode::Leaf(_) => false,
806            PaneNode::Split {
807                direction,
808                ratio,
809                first,
810                second,
811            } => {
812                // Check if this is the target divider
813                if *current_index == target_index {
814                    // Calculate new ratio based on mouse position
815                    let new_ratio = match direction {
816                        SplitDirection::Horizontal => {
817                            // Horizontal split: mouse Y position determines ratio
818                            ((new_y - bounds.y) / bounds.height).clamp(0.1, 0.9)
819                        }
820                        SplitDirection::Vertical => {
821                            // Vertical split: mouse X position determines ratio
822                            ((new_x - bounds.x) / bounds.width).clamp(0.1, 0.9)
823                        }
824                    };
825                    *ratio = new_ratio;
826                    return true;
827                }
828                *current_index += 1;
829
830                // Calculate child bounds to recurse
831                let (first_bounds, second_bounds) = match direction {
832                    SplitDirection::Horizontal => {
833                        let first_height = (bounds.height - divider_width) * *ratio;
834                        let second_height = bounds.height - first_height - divider_width;
835                        (
836                            PaneBounds::new(bounds.x, bounds.y, bounds.width, first_height),
837                            PaneBounds::new(
838                                bounds.x,
839                                bounds.y + first_height + divider_width,
840                                bounds.width,
841                                second_height,
842                            ),
843                        )
844                    }
845                    SplitDirection::Vertical => {
846                        let first_width = (bounds.width - divider_width) * *ratio;
847                        let second_width = bounds.width - first_width - divider_width;
848                        (
849                            PaneBounds::new(bounds.x, bounds.y, first_width, bounds.height),
850                            PaneBounds::new(
851                                bounds.x + first_width + divider_width,
852                                bounds.y,
853                                second_width,
854                                bounds.height,
855                            ),
856                        )
857                    }
858                };
859
860                // Try children
861                if Self::update_divider_ratio(
862                    first,
863                    target_index,
864                    current_index,
865                    is_horizontal,
866                    new_x,
867                    new_y,
868                    first_bounds,
869                    divider_width,
870                ) {
871                    return true;
872                }
873                Self::update_divider_ratio(
874                    second,
875                    target_index,
876                    current_index,
877                    is_horizontal,
878                    new_x,
879                    new_y,
880                    second_bounds,
881                    divider_width,
882                )
883            }
884        }
885    }
886
887    // =========================================================================
888    // Session Restore
889    // =========================================================================
890
891    /// Build a pane tree from a saved session layout
892    ///
893    /// Recursively constructs live `PaneNode` tree from a `SessionPaneNode`,
894    /// creating new terminal panes for each leaf. If a leaf's CWD no longer
895    /// exists, falls back to `$HOME`.
896    pub fn build_from_layout(
897        &mut self,
898        layout: &SessionPaneNode,
899        config: &Config,
900        runtime: Arc<Runtime>,
901    ) -> Result<()> {
902        let root = self.build_node_from_layout(layout, config, runtime)?;
903        let first_id = root.all_pane_ids().first().copied();
904        self.root = Some(root);
905        self.focused_pane_id = first_id;
906        self.recalculate_bounds();
907
908        // Apply per-pane backgrounds from config to restored panes
909        let panes = self.all_panes_mut();
910        for (index, pane) in panes.into_iter().enumerate() {
911            if let Some((image_path, mode, opacity)) = config.get_pane_background(index) {
912                pane.set_background(crate::pane::PaneBackground {
913                    image_path: Some(image_path),
914                    mode,
915                    opacity,
916                });
917            }
918        }
919
920        Ok(())
921    }
922
923    /// Recursively build a PaneNode from a SessionPaneNode
924    fn build_node_from_layout(
925        &mut self,
926        layout: &SessionPaneNode,
927        config: &Config,
928        runtime: Arc<Runtime>,
929    ) -> Result<PaneNode> {
930        match layout {
931            SessionPaneNode::Leaf { cwd } => {
932                let id = self.next_pane_id;
933                self.next_pane_id += 1;
934
935                let validated_cwd = crate::session::restore::validate_cwd(cwd);
936                let pane = Pane::new(id, config, runtime, validated_cwd)?;
937                Ok(PaneNode::leaf(pane))
938            }
939            SessionPaneNode::Split {
940                direction,
941                ratio,
942                first,
943                second,
944            } => {
945                let first_node = self.build_node_from_layout(first, config, runtime.clone())?;
946                let second_node = self.build_node_from_layout(second, config, runtime)?;
947                Ok(PaneNode::split(*direction, *ratio, first_node, second_node))
948            }
949        }
950    }
951
952    // =========================================================================
953    // tmux Layout Integration
954    // =========================================================================
955
956    /// Set the pane tree from a tmux layout
957    ///
958    /// This replaces the entire pane tree with one constructed from the tmux layout.
959    /// Returns a mapping of tmux pane IDs to native pane IDs.
960    ///
961    /// # Arguments
962    /// * `layout` - The parsed tmux layout
963    /// * `config` - Configuration for creating panes
964    /// * `runtime` - Async runtime for pane tasks
965    pub fn set_from_tmux_layout(
966        &mut self,
967        layout: &TmuxLayout,
968        config: &Config,
969        runtime: Arc<Runtime>,
970    ) -> Result<HashMap<TmuxPaneId, PaneId>> {
971        let mut pane_mappings = HashMap::new();
972
973        // Convert the tmux layout to our pane tree
974        let new_root =
975            self.convert_layout_node(&layout.root, config, runtime.clone(), &mut pane_mappings)?;
976
977        // Replace the root
978        self.root = Some(new_root);
979
980        // Set focus to the first pane in the mapping
981        if let Some(first_native_id) = pane_mappings.values().next() {
982            self.focused_pane_id = Some(*first_native_id);
983        }
984
985        // Update next_pane_id to avoid conflicts
986        if let Some(max_id) = pane_mappings.values().max() {
987            self.next_pane_id = max_id + 1;
988        }
989
990        // Recalculate bounds
991        self.recalculate_bounds();
992
993        log::info!(
994            "Set pane tree from tmux layout: {} panes",
995            pane_mappings.len()
996        );
997
998        Ok(pane_mappings)
999    }
1000
1001    /// Rebuild the pane tree from a tmux layout, preserving existing pane terminals
1002    ///
1003    /// This is called when panes are added or the layout structure changes.
1004    /// It rebuilds the entire tree structure to match the tmux layout while
1005    /// reusing existing Pane objects to preserve their terminal state.
1006    ///
1007    /// # Arguments
1008    /// * `layout` - The parsed tmux layout
1009    /// * `existing_mappings` - Map from tmux pane ID to native pane ID for panes to preserve
1010    /// * `new_tmux_panes` - List of new tmux pane IDs that need new Pane objects
1011    /// * `config` - Configuration for creating new panes
1012    /// * `runtime` - Async runtime for new pane tasks
1013    ///
1014    /// # Returns
1015    /// Updated mapping of tmux pane IDs to native pane IDs
1016    pub fn rebuild_from_tmux_layout(
1017        &mut self,
1018        layout: &TmuxLayout,
1019        existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1020        new_tmux_panes: &[TmuxPaneId],
1021        config: &Config,
1022        runtime: Arc<Runtime>,
1023    ) -> Result<HashMap<TmuxPaneId, PaneId>> {
1024        // Extract all existing panes from the current tree
1025        let mut existing_panes: HashMap<PaneId, Pane> = HashMap::new();
1026        if let Some(root) = self.root.take() {
1027            Self::extract_panes_from_node(root, &mut existing_panes);
1028        }
1029
1030        log::debug!(
1031            "Rebuilding layout: extracted {} existing panes, expecting {} new tmux panes",
1032            existing_panes.len(),
1033            new_tmux_panes.len()
1034        );
1035
1036        // Build the new tree structure from the tmux layout
1037        let mut new_mappings = HashMap::new();
1038        let new_root = self.rebuild_layout_node(
1039            &layout.root,
1040            existing_mappings,
1041            new_tmux_panes,
1042            &mut existing_panes,
1043            config,
1044            runtime.clone(),
1045            &mut new_mappings,
1046        )?;
1047
1048        // Replace the root
1049        self.root = Some(new_root);
1050
1051        // Set focus to the first pane if not already set
1052        if self.focused_pane_id.is_none()
1053            && let Some(first_native_id) = new_mappings.values().next()
1054        {
1055            self.focused_pane_id = Some(*first_native_id);
1056        }
1057
1058        // Update next_pane_id to avoid conflicts
1059        if let Some(max_id) = new_mappings.values().max()
1060            && *max_id >= self.next_pane_id
1061        {
1062            self.next_pane_id = max_id + 1;
1063        }
1064
1065        // Recalculate bounds
1066        self.recalculate_bounds();
1067
1068        log::info!(
1069            "Rebuilt pane tree from tmux layout: {} panes",
1070            new_mappings.len()
1071        );
1072
1073        Ok(new_mappings)
1074    }
1075
1076    /// Extract all panes from a node into a map
1077    fn extract_panes_from_node(node: PaneNode, panes: &mut HashMap<PaneId, Pane>) {
1078        match node {
1079            PaneNode::Leaf(pane) => {
1080                let pane = *pane; // Unbox the pane
1081                panes.insert(pane.id, pane);
1082            }
1083            PaneNode::Split { first, second, .. } => {
1084                Self::extract_panes_from_node(*first, panes);
1085                Self::extract_panes_from_node(*second, panes);
1086            }
1087        }
1088    }
1089
1090    /// Rebuild a layout node, reusing existing panes where possible
1091    #[allow(clippy::too_many_arguments)]
1092    fn rebuild_layout_node(
1093        &mut self,
1094        node: &LayoutNode,
1095        existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1096        new_tmux_panes: &[TmuxPaneId],
1097        existing_panes: &mut HashMap<PaneId, Pane>,
1098        config: &Config,
1099        runtime: Arc<Runtime>,
1100        new_mappings: &mut HashMap<TmuxPaneId, PaneId>,
1101    ) -> Result<PaneNode> {
1102        match node {
1103            LayoutNode::Pane { id: tmux_id, .. } => {
1104                // Check if this is an existing pane we can reuse
1105                if let Some(&native_id) = existing_mappings.get(tmux_id)
1106                    && let Some(pane) = existing_panes.remove(&native_id)
1107                {
1108                    log::debug!(
1109                        "Reusing existing pane {} for tmux pane %{}",
1110                        native_id,
1111                        tmux_id
1112                    );
1113                    new_mappings.insert(*tmux_id, native_id);
1114                    return Ok(PaneNode::leaf(pane));
1115                }
1116
1117                // This is a new pane - create it
1118                if new_tmux_panes.contains(tmux_id) {
1119                    let native_id = self.next_pane_id;
1120                    self.next_pane_id += 1;
1121
1122                    let pane = Pane::new_for_tmux(native_id, config, runtime)?;
1123                    log::debug!("Created new pane {} for tmux pane %{}", native_id, tmux_id);
1124                    new_mappings.insert(*tmux_id, native_id);
1125                    return Ok(PaneNode::leaf(pane));
1126                }
1127
1128                // Fallback - create a new pane (shouldn't happen normally)
1129                log::warn!("Unexpected tmux pane %{} - creating new pane", tmux_id);
1130                let native_id = self.next_pane_id;
1131                self.next_pane_id += 1;
1132                let pane = Pane::new_for_tmux(native_id, config, runtime)?;
1133                new_mappings.insert(*tmux_id, native_id);
1134                Ok(PaneNode::leaf(pane))
1135            }
1136
1137            LayoutNode::VerticalSplit {
1138                width, children, ..
1139            } => {
1140                // Vertical split = panes side by side
1141                self.rebuild_multi_split_to_binary(
1142                    children,
1143                    SplitDirection::Vertical,
1144                    *width,
1145                    existing_mappings,
1146                    new_tmux_panes,
1147                    existing_panes,
1148                    config,
1149                    runtime,
1150                    new_mappings,
1151                )
1152            }
1153
1154            LayoutNode::HorizontalSplit {
1155                height, children, ..
1156            } => {
1157                // Horizontal split = panes stacked
1158                self.rebuild_multi_split_to_binary(
1159                    children,
1160                    SplitDirection::Horizontal,
1161                    *height,
1162                    existing_mappings,
1163                    new_tmux_panes,
1164                    existing_panes,
1165                    config,
1166                    runtime,
1167                    new_mappings,
1168                )
1169            }
1170        }
1171    }
1172
1173    /// Rebuild multi-child split to binary, reusing existing panes
1174    #[allow(clippy::too_many_arguments)]
1175    fn rebuild_multi_split_to_binary(
1176        &mut self,
1177        children: &[LayoutNode],
1178        direction: SplitDirection,
1179        total_size: usize,
1180        existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1181        new_tmux_panes: &[TmuxPaneId],
1182        existing_panes: &mut HashMap<PaneId, Pane>,
1183        config: &Config,
1184        runtime: Arc<Runtime>,
1185        new_mappings: &mut HashMap<TmuxPaneId, PaneId>,
1186    ) -> Result<PaneNode> {
1187        if children.is_empty() {
1188            anyhow::bail!("Empty children list in tmux layout");
1189        }
1190
1191        if children.len() == 1 {
1192            return self.rebuild_layout_node(
1193                &children[0],
1194                existing_mappings,
1195                new_tmux_panes,
1196                existing_panes,
1197                config,
1198                runtime,
1199                new_mappings,
1200            );
1201        }
1202
1203        // Calculate the size of the first child for the ratio
1204        let first_size = Self::get_node_size(&children[0], direction);
1205        let ratio = (first_size as f32) / (total_size as f32);
1206
1207        // Rebuild the first child
1208        let first = self.rebuild_layout_node(
1209            &children[0],
1210            existing_mappings,
1211            new_tmux_panes,
1212            existing_panes,
1213            config,
1214            runtime.clone(),
1215            new_mappings,
1216        )?;
1217
1218        // Calculate remaining size
1219        let remaining_size = total_size.saturating_sub(first_size + 1);
1220
1221        // Rebuild the rest recursively
1222        let second = if children.len() == 2 {
1223            self.rebuild_layout_node(
1224                &children[1],
1225                existing_mappings,
1226                new_tmux_panes,
1227                existing_panes,
1228                config,
1229                runtime,
1230                new_mappings,
1231            )?
1232        } else {
1233            self.rebuild_remaining_children(
1234                &children[1..],
1235                direction,
1236                remaining_size,
1237                existing_mappings,
1238                new_tmux_panes,
1239                existing_panes,
1240                config,
1241                runtime,
1242                new_mappings,
1243            )?
1244        };
1245
1246        Ok(PaneNode::split(direction, ratio, first, second))
1247    }
1248
1249    /// Rebuild remaining children into nested binary splits
1250    #[allow(clippy::too_many_arguments)]
1251    fn rebuild_remaining_children(
1252        &mut self,
1253        children: &[LayoutNode],
1254        direction: SplitDirection,
1255        total_size: usize,
1256        existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1257        new_tmux_panes: &[TmuxPaneId],
1258        existing_panes: &mut HashMap<PaneId, Pane>,
1259        config: &Config,
1260        runtime: Arc<Runtime>,
1261        new_mappings: &mut HashMap<TmuxPaneId, PaneId>,
1262    ) -> Result<PaneNode> {
1263        if children.len() == 1 {
1264            return self.rebuild_layout_node(
1265                &children[0],
1266                existing_mappings,
1267                new_tmux_panes,
1268                existing_panes,
1269                config,
1270                runtime,
1271                new_mappings,
1272            );
1273        }
1274
1275        let first_size = Self::get_node_size(&children[0], direction);
1276        let ratio = (first_size as f32) / (total_size as f32);
1277
1278        let first = self.rebuild_layout_node(
1279            &children[0],
1280            existing_mappings,
1281            new_tmux_panes,
1282            existing_panes,
1283            config,
1284            runtime.clone(),
1285            new_mappings,
1286        )?;
1287
1288        let remaining_size = total_size.saturating_sub(first_size + 1);
1289        let second = self.rebuild_remaining_children(
1290            &children[1..],
1291            direction,
1292            remaining_size,
1293            existing_mappings,
1294            new_tmux_panes,
1295            existing_panes,
1296            config,
1297            runtime,
1298            new_mappings,
1299        )?;
1300
1301        Ok(PaneNode::split(direction, ratio, first, second))
1302    }
1303
1304    /// Convert a tmux LayoutNode to a PaneNode
1305    fn convert_layout_node(
1306        &mut self,
1307        node: &LayoutNode,
1308        config: &Config,
1309        runtime: Arc<Runtime>,
1310        mappings: &mut HashMap<TmuxPaneId, PaneId>,
1311    ) -> Result<PaneNode> {
1312        match node {
1313            LayoutNode::Pane {
1314                id: tmux_id,
1315                width: _,
1316                height: _,
1317                x: _,
1318                y: _,
1319            } => {
1320                // Create a native pane for this tmux pane
1321                let native_id = self.next_pane_id;
1322                self.next_pane_id += 1;
1323
1324                let pane = Pane::new_for_tmux(native_id, config, runtime)?;
1325
1326                // Record the mapping
1327                mappings.insert(*tmux_id, native_id);
1328
1329                log::debug!(
1330                    "Created native pane {} for tmux pane %{}",
1331                    native_id,
1332                    tmux_id
1333                );
1334
1335                Ok(PaneNode::leaf(pane))
1336            }
1337
1338            LayoutNode::VerticalSplit {
1339                width,
1340                height: _,
1341                x: _,
1342                y: _,
1343                children,
1344            } => {
1345                // Vertical split = panes side by side
1346                self.convert_multi_split_to_binary(
1347                    children,
1348                    SplitDirection::Vertical,
1349                    *width,
1350                    config,
1351                    runtime,
1352                    mappings,
1353                )
1354            }
1355
1356            LayoutNode::HorizontalSplit {
1357                width: _,
1358                height,
1359                x: _,
1360                y: _,
1361                children,
1362            } => {
1363                // Horizontal split = panes stacked
1364                self.convert_multi_split_to_binary(
1365                    children,
1366                    SplitDirection::Horizontal,
1367                    *height,
1368                    config,
1369                    runtime,
1370                    mappings,
1371                )
1372            }
1373        }
1374    }
1375
1376    /// Convert a multi-child split to nested binary splits
1377    ///
1378    /// tmux layouts can have multiple children in a single split,
1379    /// but our pane tree uses binary splits. We convert like this:
1380    /// [A, B, C] -> Split(A, Split(B, C))
1381    fn convert_multi_split_to_binary(
1382        &mut self,
1383        children: &[LayoutNode],
1384        direction: SplitDirection,
1385        total_size: usize,
1386        config: &Config,
1387        runtime: Arc<Runtime>,
1388        mappings: &mut HashMap<TmuxPaneId, PaneId>,
1389    ) -> Result<PaneNode> {
1390        if children.is_empty() {
1391            anyhow::bail!("Empty children list in tmux layout");
1392        }
1393
1394        if children.len() == 1 {
1395            // Single child - just convert it directly
1396            return self.convert_layout_node(&children[0], config, runtime, mappings);
1397        }
1398
1399        // Calculate the size of the first child for the ratio
1400        let first_size = Self::get_node_size(&children[0], direction);
1401        let ratio = (first_size as f32) / (total_size as f32);
1402
1403        // Convert the first child
1404        let first = self.convert_layout_node(&children[0], config, runtime.clone(), mappings)?;
1405
1406        // Calculate remaining size for the rest
1407        let remaining_size = total_size.saturating_sub(first_size + 1); // -1 for divider
1408
1409        // Convert the rest recursively
1410        let second = if children.len() == 2 {
1411            self.convert_layout_node(&children[1], config, runtime, mappings)?
1412        } else {
1413            // Create a synthetic split node for the remaining children
1414            let remaining = &children[1..];
1415            self.convert_remaining_children(
1416                remaining,
1417                direction,
1418                remaining_size,
1419                config,
1420                runtime,
1421                mappings,
1422            )?
1423        };
1424
1425        Ok(PaneNode::split(direction, ratio, first, second))
1426    }
1427
1428    /// Convert remaining children into nested binary splits
1429    fn convert_remaining_children(
1430        &mut self,
1431        children: &[LayoutNode],
1432        direction: SplitDirection,
1433        total_size: usize,
1434        config: &Config,
1435        runtime: Arc<Runtime>,
1436        mappings: &mut HashMap<TmuxPaneId, PaneId>,
1437    ) -> Result<PaneNode> {
1438        if children.len() == 1 {
1439            return self.convert_layout_node(&children[0], config, runtime, mappings);
1440        }
1441
1442        let first_size = Self::get_node_size(&children[0], direction);
1443        let ratio = (first_size as f32) / (total_size as f32);
1444
1445        let first = self.convert_layout_node(&children[0], config, runtime.clone(), mappings)?;
1446
1447        let remaining_size = total_size.saturating_sub(first_size + 1);
1448        let second = self.convert_remaining_children(
1449            &children[1..],
1450            direction,
1451            remaining_size,
1452            config,
1453            runtime,
1454            mappings,
1455        )?;
1456
1457        Ok(PaneNode::split(direction, ratio, first, second))
1458    }
1459
1460    /// Get the size of a node in the given direction
1461    fn get_node_size(node: &LayoutNode, direction: SplitDirection) -> usize {
1462        match node {
1463            LayoutNode::Pane { width, height, .. } => match direction {
1464                SplitDirection::Vertical => *width,
1465                SplitDirection::Horizontal => *height,
1466            },
1467            LayoutNode::VerticalSplit { width, height, .. }
1468            | LayoutNode::HorizontalSplit { width, height, .. } => match direction {
1469                SplitDirection::Vertical => *width,
1470                SplitDirection::Horizontal => *height,
1471            },
1472        }
1473    }
1474
1475    /// Update the layout structure (ratios) from a tmux layout without recreating terminals
1476    ///
1477    /// This is called when the tmux pane IDs haven't changed but the layout
1478    /// dimensions have (e.g., due to resize or another client connecting).
1479    /// It updates the split ratios in our pane tree to match the tmux layout.
1480    pub fn update_layout_from_tmux(
1481        &mut self,
1482        layout: &TmuxLayout,
1483        pane_mappings: &HashMap<TmuxPaneId, PaneId>,
1484    ) {
1485        // Calculate ratios from the tmux layout and update our tree
1486        if let Some(ref mut root) = self.root {
1487            Self::update_node_from_tmux_layout(root, &layout.root, pane_mappings);
1488        }
1489
1490        log::debug!(
1491            "Updated pane layout ratios from tmux layout ({} panes)",
1492            pane_mappings.len()
1493        );
1494    }
1495
1496    /// Recursively update a pane node's ratios and directions from tmux layout
1497    fn update_node_from_tmux_layout(
1498        node: &mut PaneNode,
1499        tmux_node: &LayoutNode,
1500        pane_mappings: &HashMap<TmuxPaneId, PaneId>,
1501    ) {
1502        match (node, tmux_node) {
1503            // Leaf nodes - nothing to update for ratios
1504            (PaneNode::Leaf(_), LayoutNode::Pane { .. }) => {}
1505
1506            // Split node with VerticalSplit layout (panes side by side)
1507            (
1508                PaneNode::Split {
1509                    direction,
1510                    ratio,
1511                    first,
1512                    second,
1513                },
1514                LayoutNode::VerticalSplit {
1515                    width, children, ..
1516                },
1517            ) if !children.is_empty() => {
1518                // Update direction to match tmux layout
1519                if *direction != SplitDirection::Vertical {
1520                    log::debug!(
1521                        "Updating split direction from {:?} to Vertical to match tmux layout",
1522                        direction
1523                    );
1524                    *direction = SplitDirection::Vertical;
1525                }
1526
1527                // Calculate ratio from first child's width vs total
1528                let first_size = Self::get_node_size(&children[0], SplitDirection::Vertical);
1529                let total_size = *width;
1530                if total_size > 0 {
1531                    *ratio = (first_size as f32) / (total_size as f32);
1532                    log::debug!(
1533                        "Updated vertical split ratio: {} / {} = {}",
1534                        first_size,
1535                        total_size,
1536                        *ratio
1537                    );
1538                }
1539
1540                // Recursively update first child
1541                Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1542
1543                // For the second child, handle multi-child case
1544                if children.len() == 2 {
1545                    Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
1546                } else if children.len() > 2 {
1547                    // Our tree is binary but tmux has N children
1548                    // The second child is a nested split containing children[1..]
1549                    // Recursively update with remaining children treated as a nested split
1550                    Self::update_nested_split(
1551                        second,
1552                        &children[1..],
1553                        SplitDirection::Vertical,
1554                        pane_mappings,
1555                    );
1556                }
1557            }
1558
1559            // Split node with HorizontalSplit layout (panes stacked)
1560            (
1561                PaneNode::Split {
1562                    direction,
1563                    ratio,
1564                    first,
1565                    second,
1566                },
1567                LayoutNode::HorizontalSplit {
1568                    height, children, ..
1569                },
1570            ) if !children.is_empty() => {
1571                // Update direction to match tmux layout
1572                if *direction != SplitDirection::Horizontal {
1573                    log::debug!(
1574                        "Updating split direction from {:?} to Horizontal to match tmux layout",
1575                        direction
1576                    );
1577                    *direction = SplitDirection::Horizontal;
1578                }
1579
1580                // Calculate ratio from first child's height vs total
1581                let first_size = Self::get_node_size(&children[0], SplitDirection::Horizontal);
1582                let total_size = *height;
1583                if total_size > 0 {
1584                    *ratio = (first_size as f32) / (total_size as f32);
1585                    log::debug!(
1586                        "Updated horizontal split ratio: {} / {} = {}",
1587                        first_size,
1588                        total_size,
1589                        *ratio
1590                    );
1591                }
1592
1593                // Recursively update first child
1594                Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1595
1596                // For the second child, handle multi-child case
1597                if children.len() == 2 {
1598                    Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
1599                } else if children.len() > 2 {
1600                    // Our tree is binary but tmux has N children
1601                    Self::update_nested_split(
1602                        second,
1603                        &children[1..],
1604                        SplitDirection::Horizontal,
1605                        pane_mappings,
1606                    );
1607                }
1608            }
1609
1610            // Mismatched structure - log and skip
1611            _ => {
1612                log::debug!("Layout structure mismatch during update - skipping ratio update");
1613            }
1614        }
1615    }
1616
1617    /// Update a nested binary split from a flat list of tmux children
1618    fn update_nested_split(
1619        node: &mut PaneNode,
1620        children: &[LayoutNode],
1621        direction: SplitDirection,
1622        pane_mappings: &HashMap<TmuxPaneId, PaneId>,
1623    ) {
1624        if children.is_empty() {
1625            return;
1626        }
1627
1628        if children.len() == 1 {
1629            // Single child - update directly
1630            Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
1631            return;
1632        }
1633
1634        // Multiple children - node should be a split
1635        if let PaneNode::Split {
1636            ratio,
1637            first,
1638            second,
1639            ..
1640        } = node
1641        {
1642            // Calculate ratio: first child size vs remaining total
1643            let first_size = Self::get_node_size(&children[0], direction);
1644            let remaining_size: usize = children
1645                .iter()
1646                .map(|c| Self::get_node_size(c, direction))
1647                .sum();
1648
1649            if remaining_size > 0 {
1650                *ratio = (first_size as f32) / (remaining_size as f32);
1651                log::debug!(
1652                    "Updated nested split ratio: {} / {} = {}",
1653                    first_size,
1654                    remaining_size,
1655                    *ratio
1656                );
1657            }
1658
1659            // Update first child
1660            Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1661
1662            // Recurse for remaining children
1663            Self::update_nested_split(second, &children[1..], direction, pane_mappings);
1664        } else {
1665            // Node isn't a split but we expected one - update as single
1666            Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
1667        }
1668    }
1669
1670    /// Update an existing pane tree to match a new tmux layout
1671    ///
1672    /// This tries to preserve existing panes where possible and only
1673    /// creates/destroys panes as needed.
1674    ///
1675    /// Returns updated mappings (Some = new mappings, None = no changes needed)
1676    pub fn update_from_tmux_layout(
1677        &mut self,
1678        layout: &TmuxLayout,
1679        existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1680        config: &Config,
1681        runtime: Arc<Runtime>,
1682    ) -> Result<Option<HashMap<TmuxPaneId, PaneId>>> {
1683        // Get the pane IDs from the new layout
1684        let new_pane_ids: std::collections::HashSet<_> = layout.pane_ids().into_iter().collect();
1685
1686        // Check if the pane set has changed
1687        let existing_tmux_ids: std::collections::HashSet<_> =
1688            existing_mappings.keys().copied().collect();
1689
1690        if new_pane_ids == existing_tmux_ids {
1691            // Same panes, just need to update the layout structure
1692            // For now, we rebuild completely since layout changes are complex
1693            // A future optimization could preserve terminals and just restructure
1694            log::debug!("tmux layout changed but same panes - rebuilding structure");
1695        }
1696
1697        // For now, always rebuild the tree completely
1698        // A more sophisticated implementation would try to preserve terminals
1699        let new_mappings = self.set_from_tmux_layout(layout, config, runtime)?;
1700        Ok(Some(new_mappings))
1701    }
1702}
1703
1704impl Default for PaneManager {
1705    fn default() -> Self {
1706        Self::new()
1707    }
1708}
1709
1710/// Result of attempting to remove a pane from the tree
1711enum RemoveResult {
1712    /// Pane was removed, returning the new subtree (or None if empty)
1713    Removed(Option<PaneNode>),
1714    /// Pane was not found, returning the original tree
1715    NotFound(PaneNode),
1716}
1717
1718#[cfg(test)]
1719mod tests {
1720    use super::*;
1721
1722    // Note: Full tests would require mocking TerminalManager
1723    // These are placeholder tests for the manager logic
1724
1725    #[test]
1726    fn test_pane_manager_new() {
1727        let manager = PaneManager::new();
1728        assert!(manager.root.is_none());
1729        assert_eq!(manager.pane_count(), 0);
1730        assert!(!manager.has_multiple_panes());
1731    }
1732}