par_term/pane/manager/tmux_update.rs
1//! In-place tmux layout ratio and direction updates.
2//!
3//! Provides `update_layout_from_tmux`, `update_from_tmux_layout`, and their
4//! recursive helpers. These methods update the split ratios of an existing pane
5//! tree to match a new tmux layout without recreating terminal sessions.
6//!
7//! For full-replace and rebuild operations, see `tmux_layout.rs`.
8//! For creating new pane trees from tmux layouts, see `tmux_convert.rs`.
9
10use super::PaneManager;
11use crate::config::Config;
12use crate::pane::types::{PaneId, PaneNode, SplitDirection};
13use crate::tmux::{LayoutNode, TmuxLayout, TmuxPaneId};
14use anyhow::Result;
15use std::collections::HashMap;
16use std::sync::Arc;
17use tokio::runtime::Runtime;
18
19impl PaneManager {
20 /// Update the layout structure (ratios) from a tmux layout without recreating terminals
21 ///
22 /// This is called when the tmux pane IDs haven't changed but the layout
23 /// dimensions have (e.g., due to resize or another client connecting).
24 /// It updates the split ratios in our pane tree to match the tmux layout.
25 pub fn update_layout_from_tmux(
26 &mut self,
27 layout: &TmuxLayout,
28 pane_mappings: &HashMap<TmuxPaneId, PaneId>,
29 ) {
30 // Calculate ratios from the tmux layout and update our tree
31 if let Some(ref mut root) = self.root {
32 Self::update_node_from_tmux_layout(root, &layout.root, pane_mappings);
33 }
34
35 log::debug!(
36 "Updated pane layout ratios from tmux layout ({} panes)",
37 pane_mappings.len()
38 );
39 }
40
41 /// Recursively update a pane node's ratios and directions from tmux layout
42 fn update_node_from_tmux_layout(
43 node: &mut PaneNode,
44 tmux_node: &LayoutNode,
45 pane_mappings: &HashMap<TmuxPaneId, PaneId>,
46 ) {
47 match (node, tmux_node) {
48 // Leaf nodes - nothing to update for ratios
49 (PaneNode::Leaf(_), LayoutNode::Pane { .. }) => {}
50
51 // Split node with VerticalSplit layout (panes side by side)
52 (
53 PaneNode::Split {
54 direction,
55 ratio,
56 first,
57 second,
58 },
59 LayoutNode::VerticalSplit {
60 width, children, ..
61 },
62 ) if !children.is_empty() => {
63 // Update direction to match tmux layout
64 if *direction != SplitDirection::Vertical {
65 log::debug!(
66 "Updating split direction from {:?} to Vertical to match tmux layout",
67 direction
68 );
69 *direction = SplitDirection::Vertical;
70 }
71
72 // Calculate ratio from first child's width vs total
73 let first_size = Self::get_node_size(&children[0], SplitDirection::Vertical);
74 let total_size = *width;
75 if total_size > 0 {
76 *ratio = (first_size as f32) / (total_size as f32);
77 log::debug!(
78 "Updated vertical split ratio: {} / {} = {}",
79 first_size,
80 total_size,
81 *ratio
82 );
83 }
84
85 // Recursively update first child
86 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
87
88 // For the second child, handle multi-child case
89 if children.len() == 2 {
90 Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
91 } else if children.len() > 2 {
92 // Our tree is binary but tmux has N children
93 // The second child is a nested split containing children[1..]
94 // Recursively update with remaining children treated as a nested split
95 Self::update_nested_split(
96 second,
97 &children[1..],
98 SplitDirection::Vertical,
99 pane_mappings,
100 );
101 }
102 }
103
104 // Split node with HorizontalSplit layout (panes stacked)
105 (
106 PaneNode::Split {
107 direction,
108 ratio,
109 first,
110 second,
111 },
112 LayoutNode::HorizontalSplit {
113 height, children, ..
114 },
115 ) if !children.is_empty() => {
116 // Update direction to match tmux layout
117 if *direction != SplitDirection::Horizontal {
118 log::debug!(
119 "Updating split direction from {:?} to Horizontal to match tmux layout",
120 direction
121 );
122 *direction = SplitDirection::Horizontal;
123 }
124
125 // Calculate ratio from first child's height vs total
126 let first_size = Self::get_node_size(&children[0], SplitDirection::Horizontal);
127 let total_size = *height;
128 if total_size > 0 {
129 *ratio = (first_size as f32) / (total_size as f32);
130 log::debug!(
131 "Updated horizontal split ratio: {} / {} = {}",
132 first_size,
133 total_size,
134 *ratio
135 );
136 }
137
138 // Recursively update first child
139 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
140
141 // For the second child, handle multi-child case
142 if children.len() == 2 {
143 Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
144 } else if children.len() > 2 {
145 // Our tree is binary but tmux has N children
146 Self::update_nested_split(
147 second,
148 &children[1..],
149 SplitDirection::Horizontal,
150 pane_mappings,
151 );
152 }
153 }
154
155 // Mismatched structure - log and skip
156 _ => {
157 log::debug!("Layout structure mismatch during update - skipping ratio update");
158 }
159 }
160 }
161
162 /// Update a nested binary split from a flat list of tmux children
163 fn update_nested_split(
164 node: &mut PaneNode,
165 children: &[LayoutNode],
166 direction: SplitDirection,
167 pane_mappings: &HashMap<TmuxPaneId, PaneId>,
168 ) {
169 if children.is_empty() {
170 return;
171 }
172
173 if children.len() == 1 {
174 // Single child - update directly
175 Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
176 return;
177 }
178
179 // Multiple children - node should be a split
180 if let PaneNode::Split {
181 ratio,
182 first,
183 second,
184 ..
185 } = node
186 {
187 // Calculate ratio: first child size vs remaining total
188 let first_size = Self::get_node_size(&children[0], direction);
189 let remaining_size: usize = children
190 .iter()
191 .map(|c| Self::get_node_size(c, direction))
192 .sum();
193
194 if remaining_size > 0 {
195 *ratio = (first_size as f32) / (remaining_size as f32);
196 log::debug!(
197 "Updated nested split ratio: {} / {} = {}",
198 first_size,
199 remaining_size,
200 *ratio
201 );
202 }
203
204 // Update first child
205 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
206
207 // Recurse for remaining children
208 Self::update_nested_split(second, &children[1..], direction, pane_mappings);
209 } else {
210 // Node isn't a split but we expected one - update as single
211 Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
212 }
213 }
214
215 /// Update an existing pane tree to match a new tmux layout
216 ///
217 /// This tries to preserve existing panes where possible and only
218 /// creates/destroys panes as needed.
219 ///
220 /// Returns updated mappings (Some = new mappings, None = no changes needed)
221 pub fn update_from_tmux_layout(
222 &mut self,
223 layout: &TmuxLayout,
224 existing_mappings: &HashMap<TmuxPaneId, PaneId>,
225 config: &Config,
226 runtime: Arc<Runtime>,
227 ) -> Result<Option<HashMap<TmuxPaneId, PaneId>>> {
228 // Get the pane IDs from the new layout
229 let new_pane_ids: std::collections::HashSet<_> = layout.pane_ids().into_iter().collect();
230
231 // Check if the pane set has changed
232 let existing_tmux_ids: std::collections::HashSet<_> =
233 existing_mappings.keys().copied().collect();
234
235 if new_pane_ids == existing_tmux_ids {
236 // Same panes, just need to update the layout structure
237 // For now, we rebuild completely since layout changes are complex
238 // A future optimization could preserve terminals and just restructure
239 log::debug!("tmux layout changed but same panes - rebuilding structure");
240 }
241
242 // For now, always rebuild the tree completely
243 // A more sophisticated implementation would try to preserve terminals
244 let new_mappings = self.set_from_tmux_layout(layout, config, runtime)?;
245 Ok(Some(new_mappings))
246 }
247}