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, darken)) = config.get_pane_background(0) {
157 pane.set_background(crate::pane::PaneBackground {
158 image_path: Some(image_path),
159 mode,
160 opacity,
161 darken,
162 });
163 }
164
165 self.root = Some(PaneNode::leaf(pane));
166 self.focused_pane_id = Some(id);
167
168 Ok(id)
169 }
170
171 pub fn split(
175 &mut self,
176 direction: SplitDirection,
177 config: &Config,
178 runtime: Arc<Runtime>,
179 ) -> Result<Option<PaneId>> {
180 let focused_id = match self.focused_pane_id {
181 Some(id) => id,
182 None => return Ok(None),
183 };
184
185 let (working_dir, focused_bounds) = if let Some(pane) = self.focused_pane() {
187 (pane.get_cwd(), pane.bounds)
188 } else {
189 (None, self.total_bounds)
190 };
191
192 let (new_cols, new_rows) = match direction {
194 SplitDirection::Vertical => {
195 let half_width = (focused_bounds.width - self.divider_width) / 2.0;
197 let cols = (half_width / config.font_size * 1.8).floor() as usize; (cols.max(10), config.rows)
199 }
200 SplitDirection::Horizontal => {
201 let half_height = (focused_bounds.height - self.divider_width) / 2.0;
203 let rows = (half_height / (config.font_size * 1.2)).floor() as usize; (config.cols, rows.max(5))
205 }
206 };
207
208 let mut pane_config = config.clone();
210 pane_config.cols = new_cols;
211 pane_config.rows = new_rows;
212
213 let new_id = self.next_pane_id;
215 self.next_pane_id += 1;
216
217 let mut new_pane = Pane::new(new_id, &pane_config, runtime, working_dir)?;
218
219 let new_pane_index = self.pane_count(); if let Some((image_path, mode, opacity, darken)) =
223 config.get_pane_background(new_pane_index)
224 {
225 new_pane.set_background(crate::pane::PaneBackground {
226 image_path: Some(image_path),
227 mode,
228 opacity,
229 darken,
230 });
231 }
232
233 if let Some(root) = self.root.take() {
235 let (new_root, _) = Self::split_node(root, focused_id, direction, Some(new_pane));
236 self.root = Some(new_root);
237 }
238
239 self.recalculate_bounds();
241
242 self.focused_pane_id = Some(new_id);
244
245 crate::debug_info!(
246 "PANE_SPLIT",
247 "Split pane {} {:?}, created new pane {}. First(left/top)={} Second(right/bottom)={} (focused)",
248 focused_id,
249 direction,
250 new_id,
251 focused_id,
252 new_id
253 );
254
255 Ok(Some(new_id))
256 }
257
258 fn split_node(
263 node: PaneNode,
264 target_id: PaneId,
265 direction: SplitDirection,
266 new_pane: Option<Pane>,
267 ) -> (PaneNode, Option<Pane>) {
268 match node {
269 PaneNode::Leaf(pane) => {
270 if pane.id == target_id {
271 if let Some(new) = new_pane {
272 (
274 PaneNode::split(
275 direction,
276 0.5, PaneNode::leaf(*pane),
278 PaneNode::leaf(new),
279 ),
280 None,
281 )
282 } else {
283 (PaneNode::Leaf(pane), None)
285 }
286 } else {
287 (PaneNode::Leaf(pane), new_pane)
289 }
290 }
291 PaneNode::Split {
292 direction: split_dir,
293 ratio,
294 first,
295 second,
296 } => {
297 let (new_first, remaining) =
299 Self::split_node(*first, target_id, direction, new_pane);
300
301 if remaining.is_none() {
302 (
304 PaneNode::Split {
305 direction: split_dir,
306 ratio,
307 first: Box::new(new_first),
308 second,
309 },
310 None,
311 )
312 } else {
313 let (new_second, remaining) =
315 Self::split_node(*second, target_id, direction, remaining);
316 (
317 PaneNode::Split {
318 direction: split_dir,
319 ratio,
320 first: Box::new(new_first),
321 second: Box::new(new_second),
322 },
323 remaining,
324 )
325 }
326 }
327 }
328 }
329
330 pub fn close_pane(&mut self, id: PaneId) -> bool {
334 crate::debug_info!("PANE_CLOSE", "close_pane called for pane {}", id);
335
336 if let Some(root) = self.root.take() {
337 match Self::remove_pane(root, id) {
338 RemoveResult::Removed(new_root) => {
339 self.root = new_root;
340
341 if self.focused_pane_id == Some(id) {
343 let new_focus = self
344 .root
345 .as_ref()
346 .and_then(|r| r.all_pane_ids().first().copied());
347 crate::debug_info!(
348 "PANE_CLOSE",
349 "Closed focused pane {}, new focus: {:?}",
350 id,
351 new_focus
352 );
353 self.focused_pane_id = new_focus;
354 }
355
356 self.recalculate_bounds();
358
359 if let Some(ref root) = self.root {
361 for pane_id in root.all_pane_ids() {
362 if let Some(pane) = self.get_pane(pane_id) {
363 crate::debug_info!(
364 "PANE_CLOSE",
365 "Remaining pane {} bounds=({:.0},{:.0} {:.0}x{:.0})",
366 pane.id,
367 pane.bounds.x,
368 pane.bounds.y,
369 pane.bounds.width,
370 pane.bounds.height
371 );
372 }
373 }
374 }
375
376 crate::debug_info!("PANE_CLOSE", "Successfully closed pane {}", id);
377 }
378 RemoveResult::NotFound(root) => {
379 crate::debug_info!("PANE_CLOSE", "Pane {} not found in tree", id);
380 self.root = Some(root);
381 }
382 }
383 }
384
385 self.root.is_none()
386 }
387
388 fn remove_pane(node: PaneNode, target_id: PaneId) -> RemoveResult {
390 match node {
391 PaneNode::Leaf(pane) => {
392 if pane.id == target_id {
393 RemoveResult::Removed(None)
395 } else {
396 RemoveResult::NotFound(PaneNode::Leaf(pane))
397 }
398 }
399 PaneNode::Split {
400 direction,
401 ratio,
402 first,
403 second,
404 } => {
405 match Self::remove_pane(*first, target_id) {
407 RemoveResult::Removed(None) => {
408 RemoveResult::Removed(Some(*second))
411 }
412 RemoveResult::Removed(Some(new_first)) => {
413 RemoveResult::Removed(Some(PaneNode::Split {
415 direction,
416 ratio,
417 first: Box::new(new_first),
418 second,
419 }))
420 }
421 RemoveResult::NotFound(first_node) => {
422 match Self::remove_pane(*second, target_id) {
424 RemoveResult::Removed(None) => {
425 RemoveResult::Removed(Some(first_node))
428 }
429 RemoveResult::Removed(Some(new_second)) => {
430 RemoveResult::Removed(Some(PaneNode::Split {
432 direction,
433 ratio,
434 first: Box::new(first_node),
435 second: Box::new(new_second),
436 }))
437 }
438 RemoveResult::NotFound(second_node) => {
439 RemoveResult::NotFound(PaneNode::Split {
441 direction,
442 ratio,
443 first: Box::new(first_node),
444 second: Box::new(second_node),
445 })
446 }
447 }
448 }
449 }
450 }
451 }
452 }
453
454 pub fn navigate(&mut self, direction: NavigationDirection) {
456 if let Some(focused_id) = self.focused_pane_id
457 && let Some(ref root) = self.root
458 && let Some(new_id) = root.find_pane_in_direction(focused_id, direction)
459 {
460 self.focused_pane_id = Some(new_id);
461 log::debug!(
462 "Navigated {:?} from pane {} to pane {}",
463 direction,
464 focused_id,
465 new_id
466 );
467 }
468 }
469
470 pub fn focus_pane(&mut self, id: PaneId) {
472 if self
473 .root
474 .as_ref()
475 .is_some_and(|r| r.find_pane(id).is_some())
476 {
477 self.focused_pane_id = Some(id);
478 }
479 }
480
481 pub fn focus_pane_at(&mut self, x: f32, y: f32) -> Option<PaneId> {
483 if let Some(ref root) = self.root
484 && let Some(pane) = root.find_pane_at(x, y)
485 {
486 let id = pane.id;
487 self.focused_pane_id = Some(id);
488 return Some(id);
489 }
490 None
491 }
492
493 pub fn focused_pane(&self) -> Option<&Pane> {
495 self.focused_pane_id
496 .and_then(|id| self.root.as_ref()?.find_pane(id))
497 }
498
499 pub fn focused_pane_mut(&mut self) -> Option<&mut Pane> {
501 let id = self.focused_pane_id?;
502 self.root.as_mut()?.find_pane_mut(id)
503 }
504
505 pub fn focused_pane_id(&self) -> Option<PaneId> {
507 self.focused_pane_id
508 }
509
510 pub fn next_pane_id(&self) -> PaneId {
512 self.next_pane_id
513 }
514
515 pub fn add_pane_for_tmux(&mut self, pane: Pane) {
520 let pane_id = pane.id;
521
522 if pane_id >= self.next_pane_id {
524 self.next_pane_id = pane_id + 1;
525 }
526
527 if self.root.is_none() {
529 self.root = Some(PaneNode::leaf(pane));
530 self.focused_pane_id = Some(pane_id);
531 return;
532 }
533
534 if let Some(existing_root) = self.root.take() {
538 self.root = Some(PaneNode::Split {
539 direction: SplitDirection::Vertical,
540 ratio: 0.5,
541 first: Box::new(existing_root),
542 second: Box::new(PaneNode::leaf(pane)),
543 });
544 }
545
546 self.focused_pane_id = Some(pane_id);
548 }
549
550 pub fn get_pane(&self, id: PaneId) -> Option<&Pane> {
552 self.root.as_ref()?.find_pane(id)
553 }
554
555 pub fn get_pane_mut(&mut self, id: PaneId) -> Option<&mut Pane> {
557 self.root.as_mut()?.find_pane_mut(id)
558 }
559
560 pub fn all_panes(&self) -> Vec<&Pane> {
562 self.root
563 .as_ref()
564 .map(|r| r.all_panes())
565 .unwrap_or_default()
566 }
567
568 pub fn all_panes_mut(&mut self) -> Vec<&mut Pane> {
570 self.root
571 .as_mut()
572 .map(|r| r.all_panes_mut())
573 .unwrap_or_default()
574 }
575
576 pub fn collect_pane_backgrounds(&self) -> Vec<PaneBackgroundConfig> {
582 self.all_panes()
583 .iter()
584 .enumerate()
585 .filter_map(|(index, pane)| {
586 pane.background
587 .image_path
588 .as_ref()
589 .map(|path| PaneBackgroundConfig {
590 index,
591 image: path.clone(),
592 mode: pane.background.mode,
593 opacity: pane.background.opacity,
594 darken: pane.background.darken,
595 })
596 })
597 .collect()
598 }
599
600 pub fn pane_count(&self) -> usize {
602 self.root.as_ref().map(|r| r.pane_count()).unwrap_or(0)
603 }
604
605 pub fn has_multiple_panes(&self) -> bool {
607 self.pane_count() > 1
608 }
609
610 pub fn set_bounds(&mut self, bounds: PaneBounds) {
612 self.total_bounds = bounds;
613 self.recalculate_bounds();
614 }
615
616 pub fn recalculate_bounds(&mut self) {
618 if let Some(ref mut root) = self.root {
619 root.calculate_bounds(self.total_bounds, self.divider_width);
620 }
621 }
622
623 pub fn resize_all_terminals(&self, cell_width: f32, cell_height: f32) {
628 self.resize_all_terminals_with_padding(cell_width, cell_height, 0.0, 0.0);
629 }
630
631 pub fn resize_all_terminals_with_padding(
639 &self,
640 cell_width: f32,
641 cell_height: f32,
642 padding: f32,
643 height_offset: f32,
644 ) {
645 if let Some(ref root) = self.root {
646 for pane in root.all_panes() {
647 let content_width = (pane.bounds.width - padding * 2.0).max(cell_width);
649 let content_height =
650 (pane.bounds.height - padding * 2.0 - height_offset).max(cell_height);
651
652 let cols = (content_width / cell_width).floor() as usize;
653 let rows = (content_height / cell_height).floor() as usize;
654
655 pane.resize_terminal(cols.max(1), rows.max(1));
656 }
657 }
658 }
659
660 pub fn set_divider_width(&mut self, width: f32) {
662 self.divider_width = width;
663 self.recalculate_bounds();
664 }
665
666 pub fn divider_width(&self) -> f32 {
668 self.divider_width
669 }
670
671 pub fn divider_hit_padding(&self) -> f32 {
673 (self.divider_hit_width - self.divider_width).max(0.0) / 2.0
674 }
675
676 pub fn resize_split(&mut self, pane_id: PaneId, delta: f32) {
681 if let Some(ref mut root) = self.root {
682 Self::adjust_split_ratio(root, pane_id, delta);
683 self.recalculate_bounds();
684 }
685 }
686
687 fn adjust_split_ratio(node: &mut PaneNode, target_id: PaneId, delta: f32) -> bool {
689 match node {
690 PaneNode::Leaf(_) => false,
691 PaneNode::Split {
692 ratio,
693 first,
694 second,
695 ..
696 } => {
697 if first.all_pane_ids().contains(&target_id) {
699 if Self::adjust_split_ratio(first, target_id, delta) {
701 return true;
702 }
703 *ratio = (*ratio + delta).clamp(0.1, 0.9);
705 return true;
706 }
707
708 if second.all_pane_ids().contains(&target_id) {
710 if Self::adjust_split_ratio(second, target_id, delta) {
712 return true;
713 }
714 *ratio = (*ratio - delta).clamp(0.1, 0.9);
716 return true;
717 }
718
719 false
720 }
721 }
722 }
723
724 pub fn root(&self) -> Option<&PaneNode> {
726 self.root.as_ref()
727 }
728
729 pub fn root_mut(&mut self) -> Option<&mut PaneNode> {
731 self.root.as_mut()
732 }
733
734 pub fn get_dividers(&self) -> Vec<DividerRect> {
736 self.root
737 .as_ref()
738 .map(|r| r.collect_dividers(self.total_bounds, self.divider_width))
739 .unwrap_or_default()
740 }
741
742 pub fn find_divider_at(&self, x: f32, y: f32, padding: f32) -> Option<usize> {
746 let dividers = self.get_dividers();
747 for (i, divider) in dividers.iter().enumerate() {
748 if divider.contains(x, y, padding) {
749 return Some(i);
750 }
751 }
752 None
753 }
754
755 pub fn is_on_divider(&self, x: f32, y: f32) -> bool {
757 let padding = (self.divider_hit_width - self.divider_width).max(0.0) / 2.0;
758 self.find_divider_at(x, y, padding).is_some()
759 }
760
761 pub fn set_divider_hit_width(&mut self, width: f32) {
763 self.divider_hit_width = width;
764 }
765
766 pub fn get_divider(&self, index: usize) -> Option<DividerRect> {
768 self.get_dividers().get(index).copied()
769 }
770
771 pub fn drag_divider(&mut self, divider_index: usize, new_x: f32, new_y: f32) {
776 let dividers = self.get_dividers();
778 if let Some(divider) = dividers.get(divider_index) {
779 if let Some(ref mut root) = self.root {
781 let mut divider_count = 0;
782 Self::update_divider_ratio(
783 root,
784 divider_index,
785 &mut divider_count,
786 divider.is_horizontal,
787 new_x,
788 new_y,
789 self.total_bounds,
790 self.divider_width,
791 );
792 self.recalculate_bounds();
793 }
794 }
795 }
796
797 #[allow(clippy::only_used_in_recursion, clippy::too_many_arguments)]
799 fn update_divider_ratio(
800 node: &mut PaneNode,
801 target_index: usize,
802 current_index: &mut usize,
803 is_horizontal: bool,
804 new_x: f32,
805 new_y: f32,
806 bounds: PaneBounds,
807 divider_width: f32,
808 ) -> bool {
809 match node {
810 PaneNode::Leaf(_) => false,
811 PaneNode::Split {
812 direction,
813 ratio,
814 first,
815 second,
816 } => {
817 if *current_index == target_index {
819 let new_ratio = match direction {
821 SplitDirection::Horizontal => {
822 ((new_y - bounds.y) / bounds.height).clamp(0.1, 0.9)
824 }
825 SplitDirection::Vertical => {
826 ((new_x - bounds.x) / bounds.width).clamp(0.1, 0.9)
828 }
829 };
830 *ratio = new_ratio;
831 return true;
832 }
833 *current_index += 1;
834
835 let (first_bounds, second_bounds) = match direction {
837 SplitDirection::Horizontal => {
838 let first_height = (bounds.height - divider_width) * *ratio;
839 let second_height = bounds.height - first_height - divider_width;
840 (
841 PaneBounds::new(bounds.x, bounds.y, bounds.width, first_height),
842 PaneBounds::new(
843 bounds.x,
844 bounds.y + first_height + divider_width,
845 bounds.width,
846 second_height,
847 ),
848 )
849 }
850 SplitDirection::Vertical => {
851 let first_width = (bounds.width - divider_width) * *ratio;
852 let second_width = bounds.width - first_width - divider_width;
853 (
854 PaneBounds::new(bounds.x, bounds.y, first_width, bounds.height),
855 PaneBounds::new(
856 bounds.x + first_width + divider_width,
857 bounds.y,
858 second_width,
859 bounds.height,
860 ),
861 )
862 }
863 };
864
865 if Self::update_divider_ratio(
867 first,
868 target_index,
869 current_index,
870 is_horizontal,
871 new_x,
872 new_y,
873 first_bounds,
874 divider_width,
875 ) {
876 return true;
877 }
878 Self::update_divider_ratio(
879 second,
880 target_index,
881 current_index,
882 is_horizontal,
883 new_x,
884 new_y,
885 second_bounds,
886 divider_width,
887 )
888 }
889 }
890 }
891
892 pub fn build_from_layout(
902 &mut self,
903 layout: &SessionPaneNode,
904 config: &Config,
905 runtime: Arc<Runtime>,
906 ) -> Result<()> {
907 let root = self.build_node_from_layout(layout, config, runtime)?;
908 let first_id = root.all_pane_ids().first().copied();
909 self.root = Some(root);
910 self.focused_pane_id = first_id;
911 self.recalculate_bounds();
912
913 let panes = self.all_panes_mut();
915 for (index, pane) in panes.into_iter().enumerate() {
916 if let Some((image_path, mode, opacity, darken)) = config.get_pane_background(index) {
917 pane.set_background(crate::pane::PaneBackground {
918 image_path: Some(image_path),
919 mode,
920 opacity,
921 darken,
922 });
923 }
924 }
925
926 Ok(())
927 }
928
929 fn build_node_from_layout(
931 &mut self,
932 layout: &SessionPaneNode,
933 config: &Config,
934 runtime: Arc<Runtime>,
935 ) -> Result<PaneNode> {
936 match layout {
937 SessionPaneNode::Leaf { cwd } => {
938 let id = self.next_pane_id;
939 self.next_pane_id += 1;
940
941 let validated_cwd = crate::session::restore::validate_cwd(cwd);
942 let pane = Pane::new(id, config, runtime, validated_cwd)?;
943 Ok(PaneNode::leaf(pane))
944 }
945 SessionPaneNode::Split {
946 direction,
947 ratio,
948 first,
949 second,
950 } => {
951 let first_node = self.build_node_from_layout(first, config, runtime.clone())?;
952 let second_node = self.build_node_from_layout(second, config, runtime)?;
953 Ok(PaneNode::split(*direction, *ratio, first_node, second_node))
954 }
955 }
956 }
957
958 pub fn set_from_tmux_layout(
972 &mut self,
973 layout: &TmuxLayout,
974 config: &Config,
975 runtime: Arc<Runtime>,
976 ) -> Result<HashMap<TmuxPaneId, PaneId>> {
977 let mut pane_mappings = HashMap::new();
978
979 let new_root =
981 self.convert_layout_node(&layout.root, config, runtime.clone(), &mut pane_mappings)?;
982
983 self.root = Some(new_root);
985
986 if let Some(first_native_id) = pane_mappings.values().next() {
988 self.focused_pane_id = Some(*first_native_id);
989 }
990
991 if let Some(max_id) = pane_mappings.values().max() {
993 self.next_pane_id = max_id + 1;
994 }
995
996 self.recalculate_bounds();
998
999 log::info!(
1000 "Set pane tree from tmux layout: {} panes",
1001 pane_mappings.len()
1002 );
1003
1004 Ok(pane_mappings)
1005 }
1006
1007 pub fn rebuild_from_tmux_layout(
1023 &mut self,
1024 layout: &TmuxLayout,
1025 existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1026 new_tmux_panes: &[TmuxPaneId],
1027 config: &Config,
1028 runtime: Arc<Runtime>,
1029 ) -> Result<HashMap<TmuxPaneId, PaneId>> {
1030 let mut existing_panes: HashMap<PaneId, Pane> = HashMap::new();
1032 if let Some(root) = self.root.take() {
1033 Self::extract_panes_from_node(root, &mut existing_panes);
1034 }
1035
1036 log::debug!(
1037 "Rebuilding layout: extracted {} existing panes, expecting {} new tmux panes",
1038 existing_panes.len(),
1039 new_tmux_panes.len()
1040 );
1041
1042 let mut new_mappings = HashMap::new();
1044 let new_root = self.rebuild_layout_node(
1045 &layout.root,
1046 existing_mappings,
1047 new_tmux_panes,
1048 &mut existing_panes,
1049 config,
1050 runtime.clone(),
1051 &mut new_mappings,
1052 )?;
1053
1054 self.root = Some(new_root);
1056
1057 if self.focused_pane_id.is_none()
1059 && let Some(first_native_id) = new_mappings.values().next()
1060 {
1061 self.focused_pane_id = Some(*first_native_id);
1062 }
1063
1064 if let Some(max_id) = new_mappings.values().max()
1066 && *max_id >= self.next_pane_id
1067 {
1068 self.next_pane_id = max_id + 1;
1069 }
1070
1071 self.recalculate_bounds();
1073
1074 log::info!(
1075 "Rebuilt pane tree from tmux layout: {} panes",
1076 new_mappings.len()
1077 );
1078
1079 Ok(new_mappings)
1080 }
1081
1082 fn extract_panes_from_node(node: PaneNode, panes: &mut HashMap<PaneId, Pane>) {
1084 match node {
1085 PaneNode::Leaf(pane) => {
1086 let pane = *pane; panes.insert(pane.id, pane);
1088 }
1089 PaneNode::Split { first, second, .. } => {
1090 Self::extract_panes_from_node(*first, panes);
1091 Self::extract_panes_from_node(*second, panes);
1092 }
1093 }
1094 }
1095
1096 #[allow(clippy::too_many_arguments)]
1098 fn rebuild_layout_node(
1099 &mut self,
1100 node: &LayoutNode,
1101 existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1102 new_tmux_panes: &[TmuxPaneId],
1103 existing_panes: &mut HashMap<PaneId, Pane>,
1104 config: &Config,
1105 runtime: Arc<Runtime>,
1106 new_mappings: &mut HashMap<TmuxPaneId, PaneId>,
1107 ) -> Result<PaneNode> {
1108 match node {
1109 LayoutNode::Pane { id: tmux_id, .. } => {
1110 if let Some(&native_id) = existing_mappings.get(tmux_id)
1112 && let Some(pane) = existing_panes.remove(&native_id)
1113 {
1114 log::debug!(
1115 "Reusing existing pane {} for tmux pane %{}",
1116 native_id,
1117 tmux_id
1118 );
1119 new_mappings.insert(*tmux_id, native_id);
1120 return Ok(PaneNode::leaf(pane));
1121 }
1122
1123 if new_tmux_panes.contains(tmux_id) {
1125 let native_id = self.next_pane_id;
1126 self.next_pane_id += 1;
1127
1128 let pane = Pane::new_for_tmux(native_id, config, runtime)?;
1129 log::debug!("Created new pane {} for tmux pane %{}", native_id, tmux_id);
1130 new_mappings.insert(*tmux_id, native_id);
1131 return Ok(PaneNode::leaf(pane));
1132 }
1133
1134 log::warn!("Unexpected tmux pane %{} - creating new pane", tmux_id);
1136 let native_id = self.next_pane_id;
1137 self.next_pane_id += 1;
1138 let pane = Pane::new_for_tmux(native_id, config, runtime)?;
1139 new_mappings.insert(*tmux_id, native_id);
1140 Ok(PaneNode::leaf(pane))
1141 }
1142
1143 LayoutNode::VerticalSplit {
1144 width, children, ..
1145 } => {
1146 self.rebuild_multi_split_to_binary(
1148 children,
1149 SplitDirection::Vertical,
1150 *width,
1151 existing_mappings,
1152 new_tmux_panes,
1153 existing_panes,
1154 config,
1155 runtime,
1156 new_mappings,
1157 )
1158 }
1159
1160 LayoutNode::HorizontalSplit {
1161 height, children, ..
1162 } => {
1163 self.rebuild_multi_split_to_binary(
1165 children,
1166 SplitDirection::Horizontal,
1167 *height,
1168 existing_mappings,
1169 new_tmux_panes,
1170 existing_panes,
1171 config,
1172 runtime,
1173 new_mappings,
1174 )
1175 }
1176 }
1177 }
1178
1179 #[allow(clippy::too_many_arguments)]
1181 fn rebuild_multi_split_to_binary(
1182 &mut self,
1183 children: &[LayoutNode],
1184 direction: SplitDirection,
1185 total_size: usize,
1186 existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1187 new_tmux_panes: &[TmuxPaneId],
1188 existing_panes: &mut HashMap<PaneId, Pane>,
1189 config: &Config,
1190 runtime: Arc<Runtime>,
1191 new_mappings: &mut HashMap<TmuxPaneId, PaneId>,
1192 ) -> Result<PaneNode> {
1193 if children.is_empty() {
1194 anyhow::bail!("Empty children list in tmux layout");
1195 }
1196
1197 if children.len() == 1 {
1198 return self.rebuild_layout_node(
1199 &children[0],
1200 existing_mappings,
1201 new_tmux_panes,
1202 existing_panes,
1203 config,
1204 runtime,
1205 new_mappings,
1206 );
1207 }
1208
1209 let first_size = Self::get_node_size(&children[0], direction);
1211 let ratio = (first_size as f32) / (total_size as f32);
1212
1213 let first = self.rebuild_layout_node(
1215 &children[0],
1216 existing_mappings,
1217 new_tmux_panes,
1218 existing_panes,
1219 config,
1220 runtime.clone(),
1221 new_mappings,
1222 )?;
1223
1224 let remaining_size = total_size.saturating_sub(first_size + 1);
1226
1227 let second = if children.len() == 2 {
1229 self.rebuild_layout_node(
1230 &children[1],
1231 existing_mappings,
1232 new_tmux_panes,
1233 existing_panes,
1234 config,
1235 runtime,
1236 new_mappings,
1237 )?
1238 } else {
1239 self.rebuild_remaining_children(
1240 &children[1..],
1241 direction,
1242 remaining_size,
1243 existing_mappings,
1244 new_tmux_panes,
1245 existing_panes,
1246 config,
1247 runtime,
1248 new_mappings,
1249 )?
1250 };
1251
1252 Ok(PaneNode::split(direction, ratio, first, second))
1253 }
1254
1255 #[allow(clippy::too_many_arguments)]
1257 fn rebuild_remaining_children(
1258 &mut self,
1259 children: &[LayoutNode],
1260 direction: SplitDirection,
1261 total_size: usize,
1262 existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1263 new_tmux_panes: &[TmuxPaneId],
1264 existing_panes: &mut HashMap<PaneId, Pane>,
1265 config: &Config,
1266 runtime: Arc<Runtime>,
1267 new_mappings: &mut HashMap<TmuxPaneId, PaneId>,
1268 ) -> Result<PaneNode> {
1269 if children.len() == 1 {
1270 return self.rebuild_layout_node(
1271 &children[0],
1272 existing_mappings,
1273 new_tmux_panes,
1274 existing_panes,
1275 config,
1276 runtime,
1277 new_mappings,
1278 );
1279 }
1280
1281 let first_size = Self::get_node_size(&children[0], direction);
1282 let ratio = (first_size as f32) / (total_size as f32);
1283
1284 let first = self.rebuild_layout_node(
1285 &children[0],
1286 existing_mappings,
1287 new_tmux_panes,
1288 existing_panes,
1289 config,
1290 runtime.clone(),
1291 new_mappings,
1292 )?;
1293
1294 let remaining_size = total_size.saturating_sub(first_size + 1);
1295 let second = self.rebuild_remaining_children(
1296 &children[1..],
1297 direction,
1298 remaining_size,
1299 existing_mappings,
1300 new_tmux_panes,
1301 existing_panes,
1302 config,
1303 runtime,
1304 new_mappings,
1305 )?;
1306
1307 Ok(PaneNode::split(direction, ratio, first, second))
1308 }
1309
1310 fn convert_layout_node(
1312 &mut self,
1313 node: &LayoutNode,
1314 config: &Config,
1315 runtime: Arc<Runtime>,
1316 mappings: &mut HashMap<TmuxPaneId, PaneId>,
1317 ) -> Result<PaneNode> {
1318 match node {
1319 LayoutNode::Pane {
1320 id: tmux_id,
1321 width: _,
1322 height: _,
1323 x: _,
1324 y: _,
1325 } => {
1326 let native_id = self.next_pane_id;
1328 self.next_pane_id += 1;
1329
1330 let pane = Pane::new_for_tmux(native_id, config, runtime)?;
1331
1332 mappings.insert(*tmux_id, native_id);
1334
1335 log::debug!(
1336 "Created native pane {} for tmux pane %{}",
1337 native_id,
1338 tmux_id
1339 );
1340
1341 Ok(PaneNode::leaf(pane))
1342 }
1343
1344 LayoutNode::VerticalSplit {
1345 width,
1346 height: _,
1347 x: _,
1348 y: _,
1349 children,
1350 } => {
1351 self.convert_multi_split_to_binary(
1353 children,
1354 SplitDirection::Vertical,
1355 *width,
1356 config,
1357 runtime,
1358 mappings,
1359 )
1360 }
1361
1362 LayoutNode::HorizontalSplit {
1363 width: _,
1364 height,
1365 x: _,
1366 y: _,
1367 children,
1368 } => {
1369 self.convert_multi_split_to_binary(
1371 children,
1372 SplitDirection::Horizontal,
1373 *height,
1374 config,
1375 runtime,
1376 mappings,
1377 )
1378 }
1379 }
1380 }
1381
1382 fn convert_multi_split_to_binary(
1388 &mut self,
1389 children: &[LayoutNode],
1390 direction: SplitDirection,
1391 total_size: usize,
1392 config: &Config,
1393 runtime: Arc<Runtime>,
1394 mappings: &mut HashMap<TmuxPaneId, PaneId>,
1395 ) -> Result<PaneNode> {
1396 if children.is_empty() {
1397 anyhow::bail!("Empty children list in tmux layout");
1398 }
1399
1400 if children.len() == 1 {
1401 return self.convert_layout_node(&children[0], config, runtime, mappings);
1403 }
1404
1405 let first_size = Self::get_node_size(&children[0], direction);
1407 let ratio = (first_size as f32) / (total_size as f32);
1408
1409 let first = self.convert_layout_node(&children[0], config, runtime.clone(), mappings)?;
1411
1412 let remaining_size = total_size.saturating_sub(first_size + 1); let second = if children.len() == 2 {
1417 self.convert_layout_node(&children[1], config, runtime, mappings)?
1418 } else {
1419 let remaining = &children[1..];
1421 self.convert_remaining_children(
1422 remaining,
1423 direction,
1424 remaining_size,
1425 config,
1426 runtime,
1427 mappings,
1428 )?
1429 };
1430
1431 Ok(PaneNode::split(direction, ratio, first, second))
1432 }
1433
1434 fn convert_remaining_children(
1436 &mut self,
1437 children: &[LayoutNode],
1438 direction: SplitDirection,
1439 total_size: usize,
1440 config: &Config,
1441 runtime: Arc<Runtime>,
1442 mappings: &mut HashMap<TmuxPaneId, PaneId>,
1443 ) -> Result<PaneNode> {
1444 if children.len() == 1 {
1445 return self.convert_layout_node(&children[0], config, runtime, mappings);
1446 }
1447
1448 let first_size = Self::get_node_size(&children[0], direction);
1449 let ratio = (first_size as f32) / (total_size as f32);
1450
1451 let first = self.convert_layout_node(&children[0], config, runtime.clone(), mappings)?;
1452
1453 let remaining_size = total_size.saturating_sub(first_size + 1);
1454 let second = self.convert_remaining_children(
1455 &children[1..],
1456 direction,
1457 remaining_size,
1458 config,
1459 runtime,
1460 mappings,
1461 )?;
1462
1463 Ok(PaneNode::split(direction, ratio, first, second))
1464 }
1465
1466 fn get_node_size(node: &LayoutNode, direction: SplitDirection) -> usize {
1468 match node {
1469 LayoutNode::Pane { width, height, .. } => match direction {
1470 SplitDirection::Vertical => *width,
1471 SplitDirection::Horizontal => *height,
1472 },
1473 LayoutNode::VerticalSplit { width, height, .. }
1474 | LayoutNode::HorizontalSplit { width, height, .. } => match direction {
1475 SplitDirection::Vertical => *width,
1476 SplitDirection::Horizontal => *height,
1477 },
1478 }
1479 }
1480
1481 pub fn update_layout_from_tmux(
1487 &mut self,
1488 layout: &TmuxLayout,
1489 pane_mappings: &HashMap<TmuxPaneId, PaneId>,
1490 ) {
1491 if let Some(ref mut root) = self.root {
1493 Self::update_node_from_tmux_layout(root, &layout.root, pane_mappings);
1494 }
1495
1496 log::debug!(
1497 "Updated pane layout ratios from tmux layout ({} panes)",
1498 pane_mappings.len()
1499 );
1500 }
1501
1502 fn update_node_from_tmux_layout(
1504 node: &mut PaneNode,
1505 tmux_node: &LayoutNode,
1506 pane_mappings: &HashMap<TmuxPaneId, PaneId>,
1507 ) {
1508 match (node, tmux_node) {
1509 (PaneNode::Leaf(_), LayoutNode::Pane { .. }) => {}
1511
1512 (
1514 PaneNode::Split {
1515 direction,
1516 ratio,
1517 first,
1518 second,
1519 },
1520 LayoutNode::VerticalSplit {
1521 width, children, ..
1522 },
1523 ) if !children.is_empty() => {
1524 if *direction != SplitDirection::Vertical {
1526 log::debug!(
1527 "Updating split direction from {:?} to Vertical to match tmux layout",
1528 direction
1529 );
1530 *direction = SplitDirection::Vertical;
1531 }
1532
1533 let first_size = Self::get_node_size(&children[0], SplitDirection::Vertical);
1535 let total_size = *width;
1536 if total_size > 0 {
1537 *ratio = (first_size as f32) / (total_size as f32);
1538 log::debug!(
1539 "Updated vertical split ratio: {} / {} = {}",
1540 first_size,
1541 total_size,
1542 *ratio
1543 );
1544 }
1545
1546 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1548
1549 if children.len() == 2 {
1551 Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
1552 } else if children.len() > 2 {
1553 Self::update_nested_split(
1557 second,
1558 &children[1..],
1559 SplitDirection::Vertical,
1560 pane_mappings,
1561 );
1562 }
1563 }
1564
1565 (
1567 PaneNode::Split {
1568 direction,
1569 ratio,
1570 first,
1571 second,
1572 },
1573 LayoutNode::HorizontalSplit {
1574 height, children, ..
1575 },
1576 ) if !children.is_empty() => {
1577 if *direction != SplitDirection::Horizontal {
1579 log::debug!(
1580 "Updating split direction from {:?} to Horizontal to match tmux layout",
1581 direction
1582 );
1583 *direction = SplitDirection::Horizontal;
1584 }
1585
1586 let first_size = Self::get_node_size(&children[0], SplitDirection::Horizontal);
1588 let total_size = *height;
1589 if total_size > 0 {
1590 *ratio = (first_size as f32) / (total_size as f32);
1591 log::debug!(
1592 "Updated horizontal split ratio: {} / {} = {}",
1593 first_size,
1594 total_size,
1595 *ratio
1596 );
1597 }
1598
1599 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1601
1602 if children.len() == 2 {
1604 Self::update_node_from_tmux_layout(second, &children[1], pane_mappings);
1605 } else if children.len() > 2 {
1606 Self::update_nested_split(
1608 second,
1609 &children[1..],
1610 SplitDirection::Horizontal,
1611 pane_mappings,
1612 );
1613 }
1614 }
1615
1616 _ => {
1618 log::debug!("Layout structure mismatch during update - skipping ratio update");
1619 }
1620 }
1621 }
1622
1623 fn update_nested_split(
1625 node: &mut PaneNode,
1626 children: &[LayoutNode],
1627 direction: SplitDirection,
1628 pane_mappings: &HashMap<TmuxPaneId, PaneId>,
1629 ) {
1630 if children.is_empty() {
1631 return;
1632 }
1633
1634 if children.len() == 1 {
1635 Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
1637 return;
1638 }
1639
1640 if let PaneNode::Split {
1642 ratio,
1643 first,
1644 second,
1645 ..
1646 } = node
1647 {
1648 let first_size = Self::get_node_size(&children[0], direction);
1650 let remaining_size: usize = children
1651 .iter()
1652 .map(|c| Self::get_node_size(c, direction))
1653 .sum();
1654
1655 if remaining_size > 0 {
1656 *ratio = (first_size as f32) / (remaining_size as f32);
1657 log::debug!(
1658 "Updated nested split ratio: {} / {} = {}",
1659 first_size,
1660 remaining_size,
1661 *ratio
1662 );
1663 }
1664
1665 Self::update_node_from_tmux_layout(first, &children[0], pane_mappings);
1667
1668 Self::update_nested_split(second, &children[1..], direction, pane_mappings);
1670 } else {
1671 Self::update_node_from_tmux_layout(node, &children[0], pane_mappings);
1673 }
1674 }
1675
1676 pub fn update_from_tmux_layout(
1683 &mut self,
1684 layout: &TmuxLayout,
1685 existing_mappings: &HashMap<TmuxPaneId, PaneId>,
1686 config: &Config,
1687 runtime: Arc<Runtime>,
1688 ) -> Result<Option<HashMap<TmuxPaneId, PaneId>>> {
1689 let new_pane_ids: std::collections::HashSet<_> = layout.pane_ids().into_iter().collect();
1691
1692 let existing_tmux_ids: std::collections::HashSet<_> =
1694 existing_mappings.keys().copied().collect();
1695
1696 if new_pane_ids == existing_tmux_ids {
1697 log::debug!("tmux layout changed but same panes - rebuilding structure");
1701 }
1702
1703 let new_mappings = self.set_from_tmux_layout(layout, config, runtime)?;
1706 Ok(Some(new_mappings))
1707 }
1708}
1709
1710impl Default for PaneManager {
1711 fn default() -> Self {
1712 Self::new()
1713 }
1714}
1715
1716enum RemoveResult {
1718 Removed(Option<PaneNode>),
1720 NotFound(PaneNode),
1722}
1723
1724#[cfg(test)]
1725mod tests {
1726 use super::*;
1727
1728 #[test]
1732 fn test_pane_manager_new() {
1733 let manager = PaneManager::new();
1734 assert!(manager.root.is_none());
1735 assert_eq!(manager.pane_count(), 0);
1736 assert!(!manager.has_multiple_panes());
1737 }
1738}