1use 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
20pub struct PaneManager {
22 root: Option<PaneNode>,
24 focused_pane_id: Option<PaneId>,
26 next_pane_id: PaneId,
28 divider_width: f32,
30 divider_hit_width: f32,
32 total_bounds: PaneBounds,
34}
35
36impl PaneManager {
37 pub fn new() -> Self {
39 Self {
40 root: None,
41 focused_pane_id: None,
42 next_pane_id: 1,
43 divider_width: 1.0, divider_hit_width: 8.0, total_bounds: PaneBounds::default(),
46 }
47 }
48
49 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 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 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 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 let pane_config = if self.total_bounds.width > 0.0 && self.total_bounds.height > 0.0 {
103 let cell_width = config.font_size * 0.6; let cell_height = config.font_size * 1.2; let effective_bounds = match split_direction {
109 Some(SplitDirection::Vertical) => {
110 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 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 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 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 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 let (new_cols, new_rows) = match direction {
193 SplitDirection::Vertical => {
194 let half_width = (focused_bounds.width - self.divider_width) / 2.0;
196 let cols = (half_width / config.font_size * 1.8).floor() as usize; (cols.max(10), config.rows)
198 }
199 SplitDirection::Horizontal => {
200 let half_height = (focused_bounds.height - self.divider_width) / 2.0;
202 let rows = (half_height / (config.font_size * 1.2)).floor() as usize; (config.cols, rows.max(5))
204 }
205 };
206
207 let mut pane_config = config.clone();
209 pane_config.cols = new_cols;
210 pane_config.rows = new_rows;
211
212 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 let new_pane_index = self.pane_count(); 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 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 self.recalculate_bounds();
237
238 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 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 (
270 PaneNode::split(
271 direction,
272 0.5, PaneNode::leaf(*pane),
274 PaneNode::leaf(new),
275 ),
276 None,
277 )
278 } else {
279 (PaneNode::Leaf(pane), None)
281 }
282 } else {
283 (PaneNode::Leaf(pane), new_pane)
285 }
286 }
287 PaneNode::Split {
288 direction: split_dir,
289 ratio,
290 first,
291 second,
292 } => {
293 let (new_first, remaining) =
295 Self::split_node(*first, target_id, direction, new_pane);
296
297 if remaining.is_none() {
298 (
300 PaneNode::Split {
301 direction: split_dir,
302 ratio,
303 first: Box::new(new_first),
304 second,
305 },
306 None,
307 )
308 } else {
309 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 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 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 self.recalculate_bounds();
354
355 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 fn remove_pane(node: PaneNode, target_id: PaneId) -> RemoveResult {
386 match node {
387 PaneNode::Leaf(pane) => {
388 if pane.id == target_id {
389 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 match Self::remove_pane(*first, target_id) {
403 RemoveResult::Removed(None) => {
404 RemoveResult::Removed(Some(*second))
407 }
408 RemoveResult::Removed(Some(new_first)) => {
409 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 match Self::remove_pane(*second, target_id) {
420 RemoveResult::Removed(None) => {
421 RemoveResult::Removed(Some(first_node))
424 }
425 RemoveResult::Removed(Some(new_second)) => {
426 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 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 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 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 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 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 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 pub fn focused_pane_id(&self) -> Option<PaneId> {
503 self.focused_pane_id
504 }
505
506 pub fn next_pane_id(&self) -> PaneId {
508 self.next_pane_id
509 }
510
511 pub fn add_pane_for_tmux(&mut self, pane: Pane) {
516 let pane_id = pane.id;
517
518 if pane_id >= self.next_pane_id {
520 self.next_pane_id = pane_id + 1;
521 }
522
523 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 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 self.focused_pane_id = Some(pane_id);
544 }
545
546 pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
548 self.root.as_ref()?.find_pane(id)
549 }
550
551 pub fn get_pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
553 self.root.as_mut()?.find_pane_mut(id)
554 }
555
556 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 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 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 pub fn pane_count(&self) -> usize {
597 self.root.as_ref().map(|r| r.pane_count()).unwrap_or(0)
598 }
599
600 pub fn has_multiple_panes(&self) -> bool {
602 self.pane_count() > 1
603 }
604
605 pub fn set_bounds(&mut self, bounds: PaneBounds) {
607 self.total_bounds = bounds;
608 self.recalculate_bounds();
609 }
610
611 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 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 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 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 pub fn set_divider_width(&mut self, width: f32) {
657 self.divider_width = width;
658 self.recalculate_bounds();
659 }
660
661 pub fn divider_width(&self) -> f32 {
663 self.divider_width
664 }
665
666 pub fn divider_hit_padding(&self) -> f32 {
668 (self.divider_hit_width - self.divider_width).max(0.0) / 2.0
669 }
670
671 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 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 if first.all_pane_ids().contains(&target_id) {
694 if Self::adjust_split_ratio(first, target_id, delta) {
696 return true;
697 }
698 *ratio = (*ratio + delta).clamp(0.1, 0.9);
700 return true;
701 }
702
703 if second.all_pane_ids().contains(&target_id) {
705 if Self::adjust_split_ratio(second, target_id, delta) {
707 return true;
708 }
709 *ratio = (*ratio - delta).clamp(0.1, 0.9);
711 return true;
712 }
713
714 false
715 }
716 }
717 }
718
719 pub fn root(&self) -> Option<&PaneNode> {
721 self.root.as_ref()
722 }
723
724 pub fn root_mut(&mut self) -> Option<&mut PaneNode> {
726 self.root.as_mut()
727 }
728
729 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 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 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 pub fn set_divider_hit_width(&mut self, width: f32) {
758 self.divider_hit_width = width;
759 }
760
761 pub fn get_divider(&self, index: usize) -> Option<DividerRect> {
763 self.get_dividers().get(index).copied()
764 }
765
766 pub fn drag_divider(&mut self, divider_index: usize, new_x: f32, new_y: f32) {
771 let dividers = self.get_dividers();
773 if let Some(divider) = dividers.get(divider_index) {
774 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 #[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 if *current_index == target_index {
814 let new_ratio = match direction {
816 SplitDirection::Horizontal => {
817 ((new_y - bounds.y) / bounds.height).clamp(0.1, 0.9)
819 }
820 SplitDirection::Vertical => {
821 ((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 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 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 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 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 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 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 let new_root =
975 self.convert_layout_node(&layout.root, config, runtime.clone(), &mut pane_mappings)?;
976
977 self.root = Some(new_root);
979
980 if let Some(first_native_id) = pane_mappings.values().next() {
982 self.focused_pane_id = Some(*first_native_id);
983 }
984
985 if let Some(max_id) = pane_mappings.values().max() {
987 self.next_pane_id = max_id + 1;
988 }
989
990 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 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 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 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 self.root = Some(new_root);
1050
1051 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 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 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 fn extract_panes_from_node(node: PaneNode, panes: &mut HashMap<PaneId, Pane>) {
1078 match node {
1079 PaneNode::Leaf(pane) => {
1080 let pane = *pane; 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 #[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 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 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 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 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 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 #[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 let first_size = Self::get_node_size(&children[0], direction);
1205 let ratio = (first_size as f32) / (total_size as f32);
1206
1207 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 let remaining_size = total_size.saturating_sub(first_size + 1);
1220
1221 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 #[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 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 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 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 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 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 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 return self.convert_layout_node(&children[0], config, runtime, mappings);
1397 }
1398
1399 let first_size = Self::get_node_size(&children[0], direction);
1401 let ratio = (first_size as f32) / (total_size as f32);
1402
1403 let first = self.convert_layout_node(&children[0], config, runtime.clone(), mappings)?;
1405
1406 let remaining_size = total_size.saturating_sub(first_size + 1); let second = if children.len() == 2 {
1411 self.convert_layout_node(&children[1], config, runtime, mappings)?
1412 } else {
1413 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 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 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 pub fn update_layout_from_tmux(
1481 &mut self,
1482 layout: &TmuxLayout,
1483 pane_mappings: &HashMap<TmuxPaneId, PaneId>,
1484 ) {
1485 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 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 (PaneNode::Leaf(_), LayoutNode::Pane { .. }) => {}
1505
1506 (
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 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 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 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1542
1543 if children.len() == 2 {
1545 Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
1546 } else if children.len() > 2 {
1547 Self::update_nested_split(
1551 second,
1552 &children[1..],
1553 SplitDirection::Vertical,
1554 pane_mappings,
1555 );
1556 }
1557 }
1558
1559 (
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 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 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 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1595
1596 if children.len() == 2 {
1598 Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
1599 } else if children.len() > 2 {
1600 Self::update_nested_split(
1602 second,
1603 &children[1..],
1604 SplitDirection::Horizontal,
1605 pane_mappings,
1606 );
1607 }
1608 }
1609
1610 _ => {
1612 log::debug!("Layout structure mismatch during update - skipping ratio update");
1613 }
1614 }
1615 }
1616
1617 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 Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
1631 return;
1632 }
1633
1634 if let PaneNode::Split {
1636 ratio,
1637 first,
1638 second,
1639 ..
1640 } = node
1641 {
1642 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 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1661
1662 Self::update_nested_split(second, &children[1..], direction, pane_mappings);
1664 } else {
1665 Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
1667 }
1668 }
1669
1670 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 let new_pane_ids: std::collections::HashSet<_> = layout.pane_ids().into_iter().collect();
1685
1686 let existing_tmux_ids: std::collections::HashSet<_> =
1688 existing_mappings.keys().copied().collect();
1689
1690 if new_pane_ids == existing_tmux_ids {
1691 log::debug!("tmux layout changed but same panes - rebuilding structure");
1695 }
1696
1697 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
1710enum RemoveResult {
1712 Removed(Option<PaneNode>),
1714 NotFound(PaneNode),
1716}
1717
1718#[cfg(test)]
1719mod tests {
1720 use super::*;
1721
1722 #[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}