Skip to main content

par_term/pane/manager/
tmux_layout.rs

1//! Tmux layout integration for PaneManager
2//!
3//! Handles building and rebuilding the pane tree from tmux layouts.
4//! Provides both full-replace (`set_from_tmux_layout`) and incremental
5//! (`rebuild_from_tmux_layout`) operations.
6//!
7//! Related sub-modules:
8//! - `tmux_convert`: Converting new tmux layouts to pane trees (fresh creation).
9//! - `tmux_update`: In-place ratio/direction updates without recreating terminals.
10
11use super::PaneManager;
12use crate::config::Config;
13use crate::pane::tmux_helpers::{TmuxLayoutRebuildContext, extract_panes_from_node};
14use crate::pane::types::{Pane, PaneId, PaneNode, SplitDirection};
15use crate::tmux::{LayoutNode, TmuxLayout, TmuxPaneId};
16use anyhow::Result;
17use std::collections::HashMap;
18use std::sync::Arc;
19use tokio::runtime::Runtime;
20
21impl PaneManager {
22    // =========================================================================
23    // tmux Layout Integration
24    // =========================================================================
25
26    /// Set the pane tree from a tmux layout
27    ///
28    /// This replaces the entire pane tree with one constructed from the tmux layout.
29    /// Returns a mapping of tmux pane IDs to native pane IDs.
30    ///
31    /// # Arguments
32    /// * `layout` - The parsed tmux layout
33    /// * `config` - Configuration for creating panes
34    /// * `runtime` - Async runtime for pane tasks
35    pub fn set_from_tmux_layout(
36        &mut self,
37        layout: &TmuxLayout,
38        config: &Config,
39        runtime: Arc<Runtime>,
40    ) -> Result<HashMap<TmuxPaneId, PaneId>> {
41        let mut pane_mappings = HashMap::new();
42
43        // Convert the tmux layout to our pane tree
44        let new_root =
45            self.convert_layout_node(&layout.root, config, runtime.clone(), &mut pane_mappings)?;
46
47        // Replace the root
48        self.root = Some(new_root);
49
50        // Set focus to the first pane in the mapping
51        if let Some(first_native_id) = pane_mappings.values().next() {
52            self.focused_pane_id = Some(*first_native_id);
53        }
54
55        // Update next_pane_id to avoid conflicts
56        if let Some(max_id) = pane_mappings.values().max() {
57            self.next_pane_id = max_id + 1;
58        }
59
60        // Recalculate bounds
61        self.recalculate_bounds();
62
63        log::info!(
64            "Set pane tree from tmux layout: {} panes",
65            pane_mappings.len()
66        );
67
68        Ok(pane_mappings)
69    }
70
71    /// Rebuild the pane tree from a tmux layout, preserving existing pane terminals
72    ///
73    /// This is called when panes are added or the layout structure changes.
74    /// It rebuilds the entire tree structure to match the tmux layout while
75    /// reusing existing Pane objects to preserve their terminal state.
76    ///
77    /// # Arguments
78    /// * `layout` - The parsed tmux layout
79    /// * `existing_mappings` - Map from tmux pane ID to native pane ID for panes to preserve
80    /// * `new_tmux_panes` - List of new tmux pane IDs that need new Pane objects
81    /// * `config` - Configuration for creating new panes
82    /// * `runtime` - Async runtime for new pane tasks
83    ///
84    /// # Returns
85    /// Updated mapping of tmux pane IDs to native pane IDs
86    pub fn rebuild_from_tmux_layout(
87        &mut self,
88        layout: &TmuxLayout,
89        existing_mappings: &HashMap<TmuxPaneId, PaneId>,
90        new_tmux_panes: &[TmuxPaneId],
91        config: &Config,
92        runtime: Arc<Runtime>,
93    ) -> Result<HashMap<TmuxPaneId, PaneId>> {
94        // Extract all existing panes from the current tree
95        let mut existing_panes: HashMap<PaneId, Pane> = HashMap::new();
96        if let Some(root) = self.root.take() {
97            extract_panes_from_node(root, &mut existing_panes);
98        }
99
100        log::debug!(
101            "Rebuilding layout: extracted {} existing panes, expecting {} new tmux panes",
102            existing_panes.len(),
103            new_tmux_panes.len()
104        );
105
106        // Build the new tree structure from the tmux layout
107        let mut new_mappings = HashMap::new();
108        let mut rebuild_ctx = TmuxLayoutRebuildContext {
109            existing_mappings,
110            new_tmux_panes,
111            existing_panes: &mut existing_panes,
112            config,
113            runtime: runtime.clone(),
114            new_mappings: &mut new_mappings,
115        };
116        let new_root = self.rebuild_layout_node(&layout.root, &mut rebuild_ctx)?;
117
118        // Replace the root
119        self.root = Some(new_root);
120
121        // Set focus to the first pane if not already set
122        if self.focused_pane_id.is_none()
123            && let Some(first_native_id) = new_mappings.values().next()
124        {
125            self.focused_pane_id = Some(*first_native_id);
126        }
127
128        // Update next_pane_id to avoid conflicts
129        if let Some(max_id) = new_mappings.values().max()
130            && *max_id >= self.next_pane_id
131        {
132            self.next_pane_id = max_id + 1;
133        }
134
135        // Recalculate bounds
136        self.recalculate_bounds();
137
138        log::info!(
139            "Rebuilt pane tree from tmux layout: {} panes",
140            new_mappings.len()
141        );
142
143        Ok(new_mappings)
144    }
145
146    /// Rebuild a layout node, reusing existing panes where possible
147    fn rebuild_layout_node(
148        &mut self,
149        node: &LayoutNode,
150        ctx: &mut TmuxLayoutRebuildContext<'_>,
151    ) -> Result<PaneNode> {
152        match node {
153            LayoutNode::Pane { id: tmux_id, .. } => {
154                // Check if this is an existing pane we can reuse
155                if let Some(&native_id) = ctx.existing_mappings.get(tmux_id)
156                    && let Some(pane) = ctx.existing_panes.remove(&native_id)
157                {
158                    log::debug!(
159                        "Reusing existing pane {} for tmux pane %{}",
160                        native_id,
161                        tmux_id
162                    );
163                    ctx.new_mappings.insert(*tmux_id, native_id);
164                    return Ok(PaneNode::leaf(pane));
165                }
166
167                // This is a new pane - create it
168                if ctx.new_tmux_panes.contains(tmux_id) {
169                    let native_id = self.next_pane_id;
170                    self.next_pane_id += 1;
171
172                    let pane = Pane::new_for_tmux(native_id, ctx.config, ctx.runtime.clone())?;
173                    log::debug!("Created new pane {} for tmux pane %{}", native_id, tmux_id);
174                    ctx.new_mappings.insert(*tmux_id, native_id);
175                    return Ok(PaneNode::leaf(pane));
176                }
177
178                // Fallback - create a new pane (shouldn't happen normally)
179                log::warn!("Unexpected tmux pane %{} - creating new pane", tmux_id);
180                let native_id = self.next_pane_id;
181                self.next_pane_id += 1;
182                let pane = Pane::new_for_tmux(native_id, ctx.config, ctx.runtime.clone())?;
183                ctx.new_mappings.insert(*tmux_id, native_id);
184                Ok(PaneNode::leaf(pane))
185            }
186
187            LayoutNode::VerticalSplit {
188                width, children, ..
189            } => {
190                // Vertical split = panes side by side
191                self.rebuild_multi_split_to_binary(children, SplitDirection::Vertical, *width, ctx)
192            }
193
194            LayoutNode::HorizontalSplit {
195                height, children, ..
196            } => {
197                // Horizontal split = panes stacked
198                self.rebuild_multi_split_to_binary(
199                    children,
200                    SplitDirection::Horizontal,
201                    *height,
202                    ctx,
203                )
204            }
205        }
206    }
207
208    /// Rebuild multi-child split to binary, reusing existing panes
209    fn rebuild_multi_split_to_binary(
210        &mut self,
211        children: &[LayoutNode],
212        direction: SplitDirection,
213        total_size: usize,
214        ctx: &mut TmuxLayoutRebuildContext<'_>,
215    ) -> Result<PaneNode> {
216        if children.is_empty() {
217            anyhow::bail!("Empty children list in tmux layout");
218        }
219
220        if children.len() == 1 {
221            return self.rebuild_layout_node(&children[0], ctx);
222        }
223
224        // Calculate the size of the first child for the ratio
225        let first_size = Self::get_node_size(&children[0], direction);
226        let ratio = (first_size as f32) / (total_size as f32);
227
228        // Rebuild the first child
229        let first = self.rebuild_layout_node(&children[0], ctx)?;
230
231        // Calculate remaining size
232        let remaining_size = total_size.saturating_sub(first_size + 1);
233
234        // Rebuild the rest recursively
235        let second = if children.len() == 2 {
236            self.rebuild_layout_node(&children[1], ctx)?
237        } else {
238            self.rebuild_remaining_children(&children[1..], direction, remaining_size, ctx)?
239        };
240
241        Ok(PaneNode::split(direction, ratio, first, second))
242    }
243
244    /// Rebuild remaining children into nested binary splits
245    fn rebuild_remaining_children(
246        &mut self,
247        children: &[LayoutNode],
248        direction: SplitDirection,
249        total_size: usize,
250        ctx: &mut TmuxLayoutRebuildContext<'_>,
251    ) -> Result<PaneNode> {
252        if children.len() == 1 {
253            return self.rebuild_layout_node(&children[0], ctx);
254        }
255
256        let first_size = Self::get_node_size(&children[0], direction);
257        let ratio = (first_size as f32) / (total_size as f32);
258
259        let first = self.rebuild_layout_node(&children[0], ctx)?;
260
261        let remaining_size = total_size.saturating_sub(first_size + 1);
262        let second =
263            self.rebuild_remaining_children(&children[1..], direction, remaining_size, ctx)?;
264
265        Ok(PaneNode::split(direction, ratio, first, second))
266    }
267
268    /// Get the size of a node in the given direction
269    pub(super) fn get_node_size(node: &LayoutNode, direction: SplitDirection) -> usize {
270        match node {
271            LayoutNode::Pane { width, height, .. } => match direction {
272                SplitDirection::Vertical => *width,
273                SplitDirection::Horizontal => *height,
274            },
275            LayoutNode::VerticalSplit { width, height, .. }
276            | LayoutNode::HorizontalSplit { width, height, .. } => match direction {
277                SplitDirection::Vertical => *width,
278                SplitDirection::Horizontal => *height,
279            },
280        }
281    }
282}