1use crate::app::window_state::WindowState;
20use crate::tmux::{
21 ParserBridge, SessionState, SyncAction, TmuxLayout, TmuxNotification, TmuxSession, TmuxWindowId,
22};
23
24impl WindowState {
25 pub(crate) fn check_tmux_notifications(&mut self) -> bool {
32 if !self.config.tmux_enabled {
34 return false;
35 }
36
37 let _session = match &self.tmux_session {
39 Some(s) if s.is_gateway_active() => s,
40 _ => return false,
41 };
42
43 let gateway_tab_id = match self.tmux_gateway_tab_id {
45 Some(id) => id,
46 None => return false,
47 };
48
49 let core_notifications = if let Some(tab) = self.tab_manager.get_tab(gateway_tab_id) {
51 if let Ok(term) = tab.terminal.try_lock() {
52 term.drain_tmux_notifications()
53 } else {
54 return false;
55 }
56 } else {
57 return false;
58 };
59
60 if core_notifications.is_empty() {
61 return false;
62 }
63
64 for (i, notif) in core_notifications.iter().enumerate() {
66 crate::debug_info!(
67 "TMUX",
68 "Core notification {}/{}: {:?}",
69 i + 1,
70 core_notifications.len(),
71 notif
72 );
73 }
74
75 let notifications = ParserBridge::convert_all(core_notifications);
77 if notifications.is_empty() {
78 crate::debug_trace!(
79 "TMUX",
80 "All core notifications were filtered out by parser bridge"
81 );
82 return false;
83 }
84
85 crate::debug_info!(
86 "TMUX",
87 "Processing {} tmux notifications (gateway mode)",
88 notifications.len()
89 );
90
91 let mut needs_redraw = false;
92
93 for notification in ¬ifications {
95 crate::debug_trace!("TMUX", "Processing notification: {:?}", notification);
96 if let Some(session) = &mut self.tmux_session
97 && session.process_gateway_notification(notification)
98 {
99 crate::debug_info!(
100 "TMUX",
101 "State transition - gateway_state: {:?}, session_state: {:?}",
102 session.gateway_state(),
103 session.state()
104 );
105 needs_redraw = true;
106 }
107 }
108
109 let mut session_notifications = Vec::new();
117 let mut layout_notifications = Vec::new();
118 let mut output_notifications = Vec::new();
119 let mut other_notifications = Vec::new();
120
121 for notification in notifications {
122 match ¬ification {
123 TmuxNotification::ControlModeStarted
124 | TmuxNotification::SessionStarted(_)
125 | TmuxNotification::SessionRenamed(_)
126 | TmuxNotification::WindowAdd(_)
127 | TmuxNotification::WindowClose(_)
128 | TmuxNotification::WindowRenamed { .. }
129 | TmuxNotification::SessionEnded => {
130 session_notifications.push(notification);
131 }
132 TmuxNotification::LayoutChange { .. } => {
133 layout_notifications.push(notification);
134 }
135 TmuxNotification::Output { .. } => {
136 output_notifications.push(notification);
137 }
138 TmuxNotification::PaneFocusChanged { .. }
139 | TmuxNotification::Error(_)
140 | TmuxNotification::Pause
141 | TmuxNotification::Continue => {
142 other_notifications.push(notification);
143 }
144 }
145 }
146
147 for notification in session_notifications {
149 match notification {
150 TmuxNotification::ControlModeStarted => {
151 crate::debug_info!("TMUX", "Control mode started - tmux is ready");
152 }
153 TmuxNotification::SessionStarted(session_name) => {
154 self.handle_tmux_session_started(&session_name);
155 needs_redraw = true;
156 }
157 TmuxNotification::SessionRenamed(session_name) => {
158 self.handle_tmux_session_renamed(&session_name);
159 needs_redraw = true;
160 }
161 TmuxNotification::WindowAdd(window_id) => {
162 self.handle_tmux_window_add(window_id);
163 needs_redraw = true;
164 }
165 TmuxNotification::WindowClose(window_id) => {
166 self.handle_tmux_window_close(window_id);
167 needs_redraw = true;
168 }
169 TmuxNotification::WindowRenamed { id, name } => {
170 self.handle_tmux_window_renamed(id, &name);
171 needs_redraw = true;
172 }
173 TmuxNotification::SessionEnded => {
174 self.handle_tmux_session_ended();
175 needs_redraw = true;
176 }
177 _ => {}
178 }
179 }
180
181 for notification in layout_notifications {
183 if let TmuxNotification::LayoutChange { window_id, layout } = notification {
184 self.handle_tmux_layout_change(window_id, &layout);
185 needs_redraw = true;
186 }
187 }
188
189 for notification in output_notifications {
191 if let TmuxNotification::Output { pane_id, data } = notification {
192 self.handle_tmux_output(pane_id, &data);
193 needs_redraw = true;
194 }
195 }
196
197 for notification in other_notifications {
199 match notification {
200 TmuxNotification::Error(msg) => {
201 self.handle_tmux_error(&msg);
202 }
203 TmuxNotification::Pause => {
204 self.handle_tmux_pause();
205 }
206 TmuxNotification::Continue => {
207 self.handle_tmux_continue();
208 needs_redraw = true;
209 }
210 TmuxNotification::PaneFocusChanged { pane_id } => {
211 self.handle_tmux_pane_focus_changed(pane_id);
212 needs_redraw = true;
213 }
214 _ => {}
215 }
216 }
217
218 needs_redraw
219 }
220
221 fn handle_tmux_session_started(&mut self, session_name: &str) {
223 crate::debug_info!("TMUX", "Session started: {}", session_name);
224
225 self.tmux_session_name = Some(session_name.to_string());
227
228 self.update_window_title_with_tmux();
230
231 self.apply_tmux_session_profile(session_name);
233
234 if let Some(gateway_tab_id) = self.tmux_gateway_tab_id
236 && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
237 {
238 tab.set_title(&format!("[tmux: {}]", session_name));
239 crate::debug_info!(
240 "TMUX",
241 "Updated gateway tab {} title to '[tmux: {}]'",
242 gateway_tab_id,
243 session_name
244 );
245 }
246
247 self.tmux_sync.enable();
249
250 let _ = self.write_to_gateway("set-option -g window-size smallest\n");
256 crate::debug_info!(
257 "TMUX",
258 "Set window-size to smallest for multi-client support"
259 );
260
261 self.send_tmux_client_size();
264
265 self.show_toast(format!("tmux: Connected to session '{}'", session_name));
270 }
271
272 fn send_tmux_client_size(&self) {
278 if let Some(renderer) = &self.renderer {
280 let (cols, rows) = renderer.grid_size();
281 let cmd = crate::tmux::TmuxCommand::set_client_size(cols, rows);
282 let cmd_str = format!("{}\n", cmd.as_str());
283
284 if self.write_to_gateway(&cmd_str) {
285 crate::debug_trace!("TMUX", "Sent client size to tmux: {}x{}", cols, rows);
286 } else {
287 crate::debug_error!("TMUX", "Failed to send client size to tmux");
288 }
289 } else {
290 crate::debug_error!("TMUX", "Cannot send client size - no renderer available");
291 }
292 }
293
294 pub fn notify_tmux_of_resize(&self) {
299 if !self.is_gateway_active() {
301 return;
302 }
303
304 self.send_tmux_client_size();
305 }
306
307 fn request_pane_refresh(&self, pane_ids: &[crate::tmux::TmuxPaneId]) {
317 for pane_id in pane_ids {
318 let cmd = format!("send-keys -t %{} C-l\n", pane_id);
321 if self.write_to_gateway(&cmd) {
322 crate::debug_trace!("TMUX", "Sent C-l to pane %{} for refresh", pane_id);
323 }
324 }
325
326 let refresh_cmd = "refresh-client\n";
328 if self.write_to_gateway(refresh_cmd) {
329 crate::debug_info!(
330 "TMUX",
331 "Requested client refresh for {} panes",
332 pane_ids.len()
333 );
334 }
335 }
336
337 fn update_window_title_with_tmux(&self) {
340 if let Some(window) = &self.window {
341 let title = if let Some(session_name) = &self.tmux_session_name {
342 format!("{} - [tmux: {}]", self.config.window_title, session_name)
343 } else {
344 self.config.window_title.clone()
345 };
346 window.set_title(&self.format_title(&title));
347 }
348 }
349
350 fn handle_tmux_session_renamed(&mut self, session_name: &str) {
352 crate::debug_info!("TMUX", "Session renamed to: {}", session_name);
353
354 self.tmux_session_name = Some(session_name.to_string());
356
357 self.update_window_title_with_tmux();
359 }
360
361 fn handle_tmux_window_add(&mut self, window_id: TmuxWindowId) {
363 crate::debug_info!("TMUX", "Window added: @{}", window_id);
364
365 if self.config.max_tabs > 0 && self.tab_manager.tab_count() >= self.config.max_tabs {
367 crate::debug_error!(
368 "TMUX",
369 "Cannot create tab for tmux window @{}: max_tabs limit ({}) reached",
370 window_id,
371 self.config.max_tabs
372 );
373 return;
374 }
375
376 let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
378
379 match self.tab_manager.new_tab(
381 &self.config,
382 std::sync::Arc::clone(&self.runtime),
383 false, grid_size,
385 ) {
386 Ok(tab_id) => {
387 crate::debug_info!(
388 "TMUX",
389 "Created tab {} for tmux window @{}",
390 tab_id,
391 window_id
392 );
393
394 self.tmux_sync.map_window(window_id, tab_id);
396
397 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
402 tab.set_title(&format!("tmux @{}", window_id));
403 if let Some(window) = &self.window {
407 tab.start_refresh_task(
408 std::sync::Arc::clone(&self.runtime),
409 std::sync::Arc::clone(window),
410 self.config.max_fps,
411 );
412 }
413
414 if let Some(renderer) = &self.renderer
416 && let Ok(mut term) = tab.terminal.try_lock()
417 {
418 let (cols, rows) = renderer.grid_size();
419 let size = renderer.size();
420 let width_px = size.width as usize;
421 let height_px = size.height as usize;
422
423 term.set_cell_dimensions(
424 renderer.cell_width() as u32,
425 renderer.cell_height() as u32,
426 );
427 let _ = term.resize_with_pixels(cols, rows, width_px, height_px);
428 crate::debug_info!(
429 "TMUX",
430 "Resized tmux tab {} terminal to {}x{}",
431 tab_id,
432 cols,
433 rows
434 );
435 }
436 }
437 }
438 Err(e) => {
439 crate::debug_error!(
440 "TMUX",
441 "Failed to create tab for tmux window @{}: {}",
442 window_id,
443 e
444 );
445 }
446 }
447 }
448
449 fn handle_tmux_window_close(&mut self, window_id: TmuxWindowId) {
451 crate::debug_info!("TMUX", "Window closed: @{}", window_id);
452
453 if let Some(tab_id) = self.tmux_sync.get_tab(window_id) {
455 crate::debug_info!(
456 "TMUX",
457 "Closing tab {} for tmux window @{}",
458 tab_id,
459 window_id
460 );
461
462 let was_last = self.tab_manager.close_tab(tab_id);
464
465 self.tmux_sync.unmap_window(window_id);
467
468 if was_last {
469 crate::debug_info!("TMUX", "Last tmux window closed, session ending");
471 self.handle_tmux_session_ended();
472 }
473 } else {
474 crate::debug_info!(
475 "TMUX",
476 "No tab mapping found for tmux window @{} (may have been created before attach)",
477 window_id
478 );
479 }
480 }
481
482 fn handle_tmux_window_renamed(&mut self, window_id: TmuxWindowId, name: &str) {
484 crate::debug_info!("TMUX", "Window @{} renamed to: {}", window_id, name);
485
486 if let Some(tab_id) = self.tmux_sync.get_tab(window_id) {
488 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
489 tab.set_title(name);
490 crate::debug_info!("TMUX", "Updated tab {} title to '{}'", tab_id, name);
491 }
492 } else {
493 crate::debug_info!(
494 "TMUX",
495 "No tab mapping found for tmux window @{} rename",
496 window_id
497 );
498 }
499 }
500
501 fn handle_tmux_layout_change(&mut self, window_id: TmuxWindowId, layout_str: &str) {
503 crate::debug_info!(
504 "TMUX",
505 "Layout changed for window @{}: {}",
506 window_id,
507 layout_str
508 );
509
510 let parsed_layout = match TmuxLayout::parse(layout_str) {
512 Some(layout) => layout,
513 None => {
514 crate::debug_error!(
515 "TMUX",
516 "Failed to parse layout string for window @{}: {}",
517 window_id,
518 layout_str
519 );
520 return;
521 }
522 };
523
524 let pane_ids = parsed_layout.pane_ids();
526 crate::debug_info!(
527 "TMUX",
528 "Parsed layout for window @{}: {} panes (IDs: {:?})",
529 window_id,
530 pane_ids.len(),
531 pane_ids
532 );
533
534 Self::log_layout_node(&parsed_layout.root, 0);
536
537 if !pane_ids.is_empty()
539 && let Some(session) = &mut self.tmux_session
540 {
541 if session.focused_pane().is_none() {
543 session.set_focused_pane(Some(pane_ids[0]));
544 }
545 }
546
547 let tab_id = if let Some(id) = self.tmux_sync.get_tab(window_id) {
549 Some(id)
550 } else {
551 let mut found_tab_id = None;
554 for pane_id in &pane_ids {
555 for tab in self.tab_manager.tabs() {
557 if tab.tmux_pane_id == Some(*pane_id) {
558 found_tab_id = Some(tab.id);
559 crate::debug_info!(
560 "TMUX",
561 "Found existing tab {} with pane %{} for window @{}",
562 tab.id,
563 pane_id,
564 window_id
565 );
566 break;
567 }
568 }
569 if found_tab_id.is_some() {
570 break;
571 }
572 }
573
574 if let Some(tid) = found_tab_id {
576 self.tmux_sync.map_window(window_id, tid);
577 crate::debug_info!(
578 "TMUX",
579 "Created window mapping: @{} -> tab {}",
580 window_id,
581 tid
582 );
583 }
584
585 found_tab_id
586 };
587
588 let is_tmux_connected = self.is_tmux_connected();
591 let status_bar_height =
592 crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux_connected);
593 let custom_status_bar_height = self.status_bar_ui.height(&self.config, self.is_fullscreen);
594
595 let bounds_info = self.renderer.as_ref().map(|r| {
596 let size = r.size();
597 let padding = r.window_padding();
598 let content_offset_y = r.content_offset_y();
599 let content_inset_right = r.content_inset_right();
600 let cell_width = r.cell_width();
601 let cell_height = r.cell_height();
602 let physical_status_bar_height =
604 (status_bar_height + custom_status_bar_height) * r.scale_factor();
605 (
606 size,
607 padding,
608 content_offset_y,
609 content_inset_right,
610 cell_width,
611 cell_height,
612 physical_status_bar_height,
613 )
614 });
615
616 if let Some(tab_id) = tab_id {
617 crate::debug_info!(
618 "TMUX",
619 "Layout change for window @{} on tab {} - {} panes: {:?}",
620 window_id,
621 tab_id,
622 pane_ids.len(),
623 pane_ids
624 );
625
626 if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
628 tab.init_pane_manager();
630
631 if let Some((
633 size,
634 padding,
635 content_offset_y,
636 content_inset_right,
637 _cell_width,
638 _cell_height,
639 status_bar_height,
640 )) = bounds_info
641 && let Some(pm) = tab.pane_manager_mut()
642 {
643 let effective_padding = if self.config.hide_window_padding_on_split {
645 0.0
646 } else {
647 padding
648 };
649 let content_width =
650 size.width as f32 - effective_padding * 2.0 - content_inset_right;
651 let content_height = size.height as f32
652 - content_offset_y
653 - effective_padding
654 - status_bar_height;
655 let bounds = crate::pane::PaneBounds::new(
656 effective_padding,
657 content_offset_y,
658 content_width,
659 content_height,
660 );
661 pm.set_bounds(bounds);
662 crate::debug_info!(
663 "TMUX",
664 "Set pane manager bounds: {}x{} at ({}, {})",
665 content_width,
666 content_height,
667 effective_padding,
668 content_offset_y
669 );
670 }
671
672 let existing_tmux_ids: std::collections::HashSet<_> =
675 self.tmux_pane_to_native_pane.keys().copied().collect();
676 let new_tmux_ids: std::collections::HashSet<_> = pane_ids.iter().copied().collect();
677
678 if existing_tmux_ids == new_tmux_ids && !existing_tmux_ids.is_empty() {
679 crate::debug_info!(
681 "TMUX",
682 "Layout change with same panes ({:?}) - preserving terminals, updating layout",
683 pane_ids
684 );
685
686 if let Some(pm) = tab.pane_manager_mut() {
688 pm.update_layout_from_tmux(&parsed_layout, &self.tmux_pane_to_native_pane);
690 pm.recalculate_bounds();
691
692 if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info {
695 pm.resize_all_terminals(cell_width, cell_height);
696 }
697 }
698
699 self.needs_redraw = true;
700 return; }
702
703 let panes_to_keep: std::collections::HashSet<_> = existing_tmux_ids
706 .intersection(&new_tmux_ids)
707 .copied()
708 .collect();
709 let panes_to_remove: Vec<_> = existing_tmux_ids
710 .difference(&new_tmux_ids)
711 .copied()
712 .collect();
713 let panes_to_add: Vec<_> = new_tmux_ids
714 .difference(&existing_tmux_ids)
715 .copied()
716 .collect();
717
718 if !panes_to_keep.is_empty()
720 && !panes_to_remove.is_empty()
721 && panes_to_add.is_empty()
722 {
723 crate::debug_info!(
724 "TMUX",
725 "Layout change: keeping {:?}, removing {:?}",
726 panes_to_keep,
727 panes_to_remove
728 );
729
730 let current_focused = self.tmux_session.as_ref().and_then(|s| s.focused_pane());
732 let focused_pane_removed = current_focused
733 .map(|fp| panes_to_remove.contains(&fp))
734 .unwrap_or(false);
735
736 if let Some(pm) = tab.pane_manager_mut() {
738 for tmux_pane_id in &panes_to_remove {
739 if let Some(native_pane_id) =
740 self.tmux_pane_to_native_pane.get(tmux_pane_id)
741 {
742 crate::debug_info!(
743 "TMUX",
744 "Removing native pane {} for closed tmux pane %{}",
745 native_pane_id,
746 tmux_pane_id
747 );
748 pm.close_pane(*native_pane_id);
749 }
750 }
751
752 let kept_mappings: std::collections::HashMap<_, _> = self
755 .tmux_pane_to_native_pane
756 .iter()
757 .filter(|(tmux_id, _)| panes_to_keep.contains(tmux_id))
758 .map(|(k, v)| (*k, *v))
759 .collect();
760
761 pm.update_layout_from_tmux(&parsed_layout, &kept_mappings);
762 pm.recalculate_bounds();
763
764 if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info {
766 pm.resize_all_terminals(cell_width, cell_height);
767 }
768 }
769
770 for tmux_pane_id in &panes_to_remove {
772 if let Some(native_id) = self.tmux_pane_to_native_pane.remove(tmux_pane_id)
773 {
774 self.native_pane_to_tmux_pane.remove(&native_id);
775 }
776 }
777
778 if focused_pane_removed
780 && let Some(new_focus) = panes_to_keep.iter().next().copied()
781 {
782 crate::debug_info!(
783 "TMUX",
784 "Focused pane was removed, updating tmux session focus to %{}",
785 new_focus
786 );
787 if let Some(session) = &mut self.tmux_session {
788 session.set_focused_pane(Some(new_focus));
789 }
790 }
791
792 crate::debug_info!(
793 "TMUX",
794 "After pane removal, mappings: {:?}",
795 self.tmux_pane_to_native_pane
796 );
797
798 self.needs_redraw = true;
799 self.request_redraw();
800 return; }
802
803 if !panes_to_keep.is_empty()
805 && !panes_to_add.is_empty()
806 && panes_to_remove.is_empty()
807 {
808 crate::debug_info!(
809 "TMUX",
810 "Layout change: keeping {:?}, adding {:?}",
811 panes_to_keep,
812 panes_to_add
813 );
814
815 if let Some(pm) = tab.pane_manager_mut() {
818 let existing_mappings: std::collections::HashMap<_, _> = panes_to_keep
820 .iter()
821 .filter_map(|tmux_id| {
822 self.tmux_pane_to_native_pane
823 .get(tmux_id)
824 .map(|native_id| (*tmux_id, *native_id))
825 })
826 .collect();
827
828 match pm.rebuild_from_tmux_layout(
829 &parsed_layout,
830 &existing_mappings,
831 &panes_to_add,
832 &self.config,
833 std::sync::Arc::clone(&self.runtime),
834 ) {
835 Ok(new_mappings) => {
836 self.tmux_pane_to_native_pane = new_mappings.clone();
838 self.native_pane_to_tmux_pane = new_mappings
839 .iter()
840 .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
841 .collect();
842
843 crate::debug_info!(
844 "TMUX",
845 "Rebuilt layout with {} panes: {:?}",
846 new_mappings.len(),
847 new_mappings
848 );
849
850 if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info
852 {
853 pm.resize_all_terminals(cell_width, cell_height);
854 }
855 }
856 Err(e) => {
857 crate::debug_error!("TMUX", "Failed to rebuild layout: {}", e);
858 }
859 }
860 }
861
862 self.request_pane_refresh(&panes_to_add);
864
865 crate::debug_info!(
866 "TMUX",
867 "After pane addition, mappings: {:?}",
868 self.tmux_pane_to_native_pane
869 );
870
871 self.needs_redraw = true;
872 self.request_redraw();
873 return; }
875
876 if let Some(pm) = tab.pane_manager_mut() {
878 crate::debug_info!(
879 "TMUX",
880 "Full layout recreation: existing={:?}, new={:?}",
881 existing_tmux_ids,
882 new_tmux_ids
883 );
884
885 match pm.set_from_tmux_layout(
886 &parsed_layout,
887 &self.config,
888 std::sync::Arc::clone(&self.runtime),
889 ) {
890 Ok(pane_mappings) => {
891 crate::debug_info!(
893 "TMUX",
894 "Storing pane mappings: {:?}",
895 pane_mappings
896 );
897 self.tmux_pane_to_native_pane = pane_mappings.clone();
899 self.native_pane_to_tmux_pane = pane_mappings
900 .iter()
901 .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
902 .collect();
903
904 crate::debug_info!(
905 "TMUX",
906 "Applied tmux layout to tab {}: {} pane mappings created",
907 tab_id,
908 pane_mappings.len()
909 );
910
911 if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
913 tab.tmux_pane_id = Some(pane_ids[0]);
914 }
915
916 self.request_pane_refresh(&pane_ids);
919
920 self.needs_redraw = true;
921 }
922 Err(e) => {
923 crate::debug_error!(
924 "TMUX",
925 "Failed to apply tmux layout to tab {}: {}",
926 tab_id,
927 e
928 );
929 if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
931 tab.tmux_pane_id = Some(pane_ids[0]);
932 }
933 }
934 }
935 } else {
936 if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
938 tab.tmux_pane_id = Some(pane_ids[0]);
939 crate::debug_info!(
940 "TMUX",
941 "Set tab {} tmux_pane_id to %{} for output routing (no pane manager)",
942 tab_id,
943 pane_ids[0]
944 );
945 }
946 }
947 }
948 } else {
949 crate::debug_info!(
951 "TMUX",
952 "No tab mapping for window @{}, creating new tab for layout",
953 window_id
954 );
955
956 if self.config.max_tabs == 0 || self.tab_manager.tab_count() < self.config.max_tabs {
958 let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
959 match self.tab_manager.new_tab(
960 &self.config,
961 std::sync::Arc::clone(&self.runtime),
962 false,
963 grid_size,
964 ) {
965 Ok(new_tab_id) => {
966 crate::debug_info!(
967 "TMUX",
968 "Created tab {} for tmux window @{}",
969 new_tab_id,
970 window_id
971 );
972
973 self.tmux_sync.map_window(window_id, new_tab_id);
975
976 if let Some(tab) = self.tab_manager.get_tab_mut(new_tab_id) {
978 tab.init_pane_manager();
979 tab.set_title(&format!("tmux @{}", window_id));
980
981 if let Some(window) = &self.window {
983 tab.start_refresh_task(
984 std::sync::Arc::clone(&self.runtime),
985 std::sync::Arc::clone(window),
986 self.config.max_fps,
987 );
988 }
989
990 if let Some((
992 size,
993 padding,
994 content_offset_y,
995 content_inset_right,
996 _cell_width,
997 _cell_height,
998 status_bar_height,
999 )) = bounds_info
1000 && let Some(pm) = tab.pane_manager_mut()
1001 {
1002 let content_width =
1003 size.width as f32 - padding * 2.0 - content_inset_right;
1004 let content_height = size.height as f32
1005 - content_offset_y
1006 - padding
1007 - status_bar_height;
1008 let bounds = crate::pane::PaneBounds::new(
1009 padding,
1010 content_offset_y,
1011 content_width,
1012 content_height,
1013 );
1014 pm.set_bounds(bounds);
1015 }
1016
1017 if let Some(pm) = tab.pane_manager_mut() {
1019 match pm.set_from_tmux_layout(
1020 &parsed_layout,
1021 &self.config,
1022 std::sync::Arc::clone(&self.runtime),
1023 ) {
1024 Ok(pane_mappings) => {
1025 crate::debug_info!(
1026 "TMUX",
1027 "Storing pane mappings for new tab: {:?}",
1028 pane_mappings
1029 );
1030 self.native_pane_to_tmux_pane = pane_mappings
1032 .iter()
1033 .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
1034 .collect();
1035 self.tmux_pane_to_native_pane = pane_mappings;
1036
1037 if !pane_ids.is_empty() {
1039 tab.tmux_pane_id = Some(pane_ids[0]);
1040 }
1041
1042 self.request_pane_refresh(&pane_ids);
1044
1045 self.needs_redraw = true;
1046 }
1047 Err(e) => {
1048 crate::debug_error!(
1049 "TMUX",
1050 "Failed to apply tmux layout to new tab: {}",
1051 e
1052 );
1053 }
1054 }
1055 }
1056 }
1057
1058 self.tab_manager.switch_to(new_tab_id);
1060 }
1061 Err(e) => {
1062 crate::debug_error!(
1063 "TMUX",
1064 "Failed to create tab for tmux window @{}: {}",
1065 window_id,
1066 e
1067 );
1068 }
1069 }
1070 }
1071 }
1072 }
1073
1074 fn log_layout_node(node: &crate::tmux::LayoutNode, depth: usize) {
1076 let indent = " ".repeat(depth);
1077 match node {
1078 crate::tmux::LayoutNode::Pane {
1079 id,
1080 width,
1081 height,
1082 x,
1083 y,
1084 } => {
1085 crate::debug_trace!(
1086 "TMUX",
1087 "{}Pane %{}: {}x{} at ({}, {})",
1088 indent,
1089 id,
1090 width,
1091 height,
1092 x,
1093 y
1094 );
1095 }
1096 crate::tmux::LayoutNode::VerticalSplit {
1097 width,
1098 height,
1099 x,
1100 y,
1101 children,
1102 } => {
1103 crate::debug_trace!(
1104 "TMUX",
1105 "{}VerticalSplit: {}x{} at ({}, {}) with {} children",
1106 indent,
1107 width,
1108 height,
1109 x,
1110 y,
1111 children.len()
1112 );
1113 for child in children {
1114 Self::log_layout_node(child, depth + 1);
1115 }
1116 }
1117 crate::tmux::LayoutNode::HorizontalSplit {
1118 width,
1119 height,
1120 x,
1121 y,
1122 children,
1123 } => {
1124 crate::debug_trace!(
1125 "TMUX",
1126 "{}HorizontalSplit: {}x{} at ({}, {}) with {} children",
1127 indent,
1128 width,
1129 height,
1130 x,
1131 y,
1132 children.len()
1133 );
1134 for child in children {
1135 Self::log_layout_node(child, depth + 1);
1136 }
1137 }
1138 }
1139 }
1140
1141 fn handle_tmux_output(&mut self, pane_id: crate::tmux::TmuxPaneId, data: &[u8]) {
1143 if data.is_empty() {
1144 return;
1145 }
1146
1147 crate::debug_trace!(
1148 "TMUX",
1149 "Output from pane %{}: {} bytes",
1150 pane_id,
1151 data.len()
1152 );
1153
1154 if data.len() <= 20 {
1156 crate::debug_trace!(
1157 "TMUX",
1158 "Output data: {:?} (hex: {:02x?})",
1159 String::from_utf8_lossy(data),
1160 data
1161 );
1162 }
1163
1164 if self.tmux_sync.buffer_output(pane_id, data) {
1166 crate::debug_trace!(
1167 "TMUX",
1168 "Buffered {} bytes for pane %{} (paused)",
1169 data.len(),
1170 pane_id
1171 );
1172 return;
1173 }
1174
1175 crate::debug_trace!("TMUX", "Pane mappings: {:?}", self.tmux_pane_to_native_pane);
1177
1178 let native_pane_id = self
1181 .tmux_pane_to_native_pane
1182 .get(&pane_id)
1183 .copied()
1184 .or_else(|| self.tmux_sync.get_native_pane(pane_id));
1185
1186 if let Some(native_pane_id) = native_pane_id {
1187 for tab in self.tab_manager.tabs_mut() {
1189 if let Some(pane_manager) = tab.pane_manager_mut()
1190 && let Some(pane) = pane_manager.get_pane_mut(native_pane_id)
1191 && let Ok(term) = pane.terminal.try_lock()
1192 {
1193 term.process_data(data);
1195 crate::debug_trace!(
1196 "TMUX",
1197 "Routed {} bytes to pane {} (tmux %{})",
1198 data.len(),
1199 native_pane_id,
1200 pane_id
1201 );
1202 return;
1203 }
1204 }
1205 }
1206
1207 for tab in self.tab_manager.tabs_mut() {
1210 if tab.tmux_pane_id == Some(pane_id)
1211 && let Ok(term) = tab.terminal.try_lock()
1212 {
1213 term.process_data(data);
1214 crate::debug_trace!(
1215 "TMUX",
1216 "Routed {} bytes to tab terminal (tmux %{})",
1217 data.len(),
1218 pane_id
1219 );
1220 return;
1221 }
1222 }
1223
1224 crate::debug_trace!(
1228 "TMUX",
1229 "No direct mapping for tmux pane %{}, looking for existing tmux tab",
1230 pane_id
1231 );
1232
1233 for tab in self.tab_manager.tabs_mut() {
1235 if tab.tmux_pane_id.is_some()
1236 && !tab.tmux_gateway_active
1237 && let Ok(term) = tab.terminal.try_lock()
1238 {
1239 term.process_data(data);
1240 crate::debug_trace!(
1241 "TMUX",
1242 "Routed {} bytes from pane %{} to existing tmux tab (pane %{:?})",
1243 data.len(),
1244 pane_id,
1245 tab.tmux_pane_id
1246 );
1247 return;
1248 }
1249 }
1250
1251 crate::debug_info!(
1253 "TMUX",
1254 "No existing tmux tab found, creating new tab for pane %{}",
1255 pane_id
1256 );
1257
1258 if self.tmux_gateway_tab_id.is_some() {
1261 if self.config.max_tabs == 0 || self.tab_manager.tab_count() < self.config.max_tabs {
1263 let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
1264 match self.tab_manager.new_tab(
1265 &self.config,
1266 std::sync::Arc::clone(&self.runtime),
1267 false,
1268 grid_size,
1269 ) {
1270 Ok(new_tab_id) => {
1271 crate::debug_info!(
1272 "TMUX",
1273 "Created tab {} for tmux pane %{}",
1274 new_tab_id,
1275 pane_id
1276 );
1277
1278 if let Some(session) = &mut self.tmux_session
1280 && session.focused_pane().is_none()
1281 {
1282 session.set_focused_pane(Some(pane_id));
1283 }
1284
1285 if let Some(tab) = self.tab_manager.get_tab_mut(new_tab_id) {
1287 tab.tmux_pane_id = Some(pane_id);
1289 tab.set_title(&format!("tmux %{}", pane_id));
1290
1291 if let Some(window) = &self.window {
1293 tab.start_refresh_task(
1294 std::sync::Arc::clone(&self.runtime),
1295 std::sync::Arc::clone(window),
1296 self.config.max_fps,
1297 );
1298 }
1299
1300 if let Ok(term) = tab.terminal.try_lock() {
1302 term.process_data(data);
1303 }
1304 }
1305
1306 self.tab_manager.switch_to(new_tab_id);
1308 }
1309 Err(e) => {
1310 crate::debug_error!(
1311 "TMUX",
1312 "Failed to create tab for tmux pane %{}: {}",
1313 pane_id,
1314 e
1315 );
1316 }
1317 }
1318 } else {
1319 crate::debug_error!(
1320 "TMUX",
1321 "Cannot create tab for tmux pane %{}: max tabs reached",
1322 pane_id
1323 );
1324 }
1325 }
1326 }
1327
1328 fn handle_tmux_pane_focus_changed(&mut self, tmux_pane_id: crate::tmux::TmuxPaneId) {
1330 crate::debug_info!("TMUX", "Pane focus changed to %{}", tmux_pane_id);
1331
1332 if let Some(session) = &mut self.tmux_session {
1334 session.set_focused_pane(Some(tmux_pane_id));
1335 }
1336
1337 if let Some(native_pane_id) = self.tmux_pane_to_native_pane.get(&tmux_pane_id) {
1339 if let Some(tab) = self.tab_manager.active_tab_mut()
1341 && let Some(pm) = tab.pane_manager_mut()
1342 {
1343 pm.focus_pane(*native_pane_id);
1344 crate::debug_info!(
1345 "TMUX",
1346 "Updated native pane focus: tmux %{} -> native {}",
1347 tmux_pane_id,
1348 native_pane_id
1349 );
1350 }
1351 }
1352 }
1353
1354 fn handle_tmux_session_ended(&mut self) {
1356 crate::debug_info!("TMUX", "Session ended");
1357
1358 let gateway_tab_id = self.tmux_gateway_tab_id;
1360 let tmux_tabs_to_close: Vec<crate::tab::TabId> = self
1361 .tab_manager
1362 .tabs()
1363 .iter()
1364 .filter_map(|tab| {
1365 if tab.tmux_pane_id.is_some() && Some(tab.id) != gateway_tab_id {
1368 Some(tab.id)
1369 } else {
1370 None
1371 }
1372 })
1373 .collect();
1374
1375 for tab_id in tmux_tabs_to_close {
1377 crate::debug_info!("TMUX", "Closing tmux display tab {}", tab_id);
1378 let _ = self.tab_manager.close_tab(tab_id);
1379 }
1380
1381 if let Some(gateway_tab_id) = self.tmux_gateway_tab_id
1383 && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
1384 && tab.tmux_gateway_active
1385 {
1386 tab.tmux_gateway_active = false;
1387 tab.tmux_pane_id = None;
1388 tab.clear_auto_profile(); if let Ok(term) = tab.terminal.try_lock() {
1390 term.set_tmux_control_mode(false);
1391 }
1392 }
1393 self.tmux_gateway_tab_id = None;
1394
1395 if let Some(mut session) = self.tmux_session.take() {
1397 session.disconnect();
1398 }
1399 self.tmux_session_name = None;
1400
1401 self.tmux_pane_to_native_pane.clear();
1403 self.native_pane_to_tmux_pane.clear();
1404
1405 self.update_window_title_with_tmux();
1407
1408 self.tmux_sync = crate::tmux::TmuxSync::new();
1410
1411 self.show_toast("tmux: Session ended");
1413 }
1414
1415 fn handle_tmux_error(&mut self, msg: &str) {
1417 crate::debug_error!("TMUX", "Error from tmux: {}", msg);
1418
1419 self.deliver_notification("tmux Error", msg);
1421 }
1422
1423 fn handle_tmux_pause(&mut self) {
1425 crate::debug_info!("TMUX", "Received pause notification - buffering output");
1426
1427 self.tmux_sync.pause();
1429
1430 self.show_toast("tmux: Output paused (slow connection)");
1432 }
1433
1434 fn handle_tmux_continue(&mut self) {
1436 crate::debug_info!("TMUX", "Received continue notification - resuming output");
1437
1438 let buffered = self.tmux_sync.resume();
1440
1441 for (tmux_pane_id, data) in buffered {
1443 if !data.is_empty() {
1444 crate::debug_info!(
1445 "TMUX",
1446 "Flushing {} buffered bytes to pane %{}",
1447 data.len(),
1448 tmux_pane_id
1449 );
1450
1451 if let Some(native_pane_id) = self.tmux_sync.get_native_pane(tmux_pane_id) {
1453 for tab in self.tab_manager.tabs_mut() {
1455 if let Some(pane_manager) = tab.pane_manager_mut()
1456 && let Some(pane) = pane_manager.get_pane_mut(native_pane_id)
1457 {
1458 if let Ok(term) = pane.terminal.try_lock() {
1459 term.process_data(&data);
1460 }
1461 break;
1462 }
1463 }
1464 }
1465 }
1466 }
1467
1468 self.show_toast("tmux: Output resumed");
1470 }
1471
1472 #[allow(dead_code)]
1474 fn process_sync_actions(&mut self, actions: Vec<SyncAction>) {
1475 for action in actions {
1476 match action {
1477 SyncAction::CreateTab { window_id } => {
1478 crate::debug_info!("TMUX", "Sync: Create tab for window @{}", window_id);
1479 }
1480 SyncAction::CloseTab { tab_id } => {
1481 crate::debug_info!("TMUX", "Sync: Close tab {}", tab_id);
1482 }
1483 SyncAction::RenameTab { tab_id, name } => {
1484 crate::debug_info!("TMUX", "Sync: Rename tab {} to '{}'", tab_id, name);
1485 }
1486 SyncAction::UpdateLayout { tab_id, layout: _ } => {
1487 crate::debug_info!("TMUX", "Sync: Update layout for tab {}", tab_id);
1488 }
1489 SyncAction::PaneOutput { pane_id, data } => {
1490 crate::debug_trace!(
1491 "TMUX",
1492 "Sync: Route {} bytes to pane {}",
1493 data.len(),
1494 pane_id
1495 );
1496 }
1497 SyncAction::SessionEnded => {
1498 crate::debug_info!("TMUX", "Sync: Session ended");
1499 self.handle_tmux_session_ended();
1500 }
1501 SyncAction::Pause => {
1502 crate::debug_info!("TMUX", "Sync: Pause");
1503 self.handle_tmux_pause();
1504 }
1505 SyncAction::Continue => {
1506 crate::debug_info!("TMUX", "Sync: Continue");
1507 self.handle_tmux_continue();
1508 }
1509 }
1510 }
1511 }
1512
1513 pub fn initiate_tmux_gateway(&mut self, session_name: Option<&str>) -> anyhow::Result<()> {
1526 if !self.config.tmux_enabled {
1527 anyhow::bail!("tmux integration is disabled");
1528 }
1529
1530 if self.tmux_session.is_some() && self.is_tmux_connected() {
1531 anyhow::bail!("Already connected to a tmux session");
1532 }
1533
1534 crate::debug_info!(
1535 "TMUX",
1536 "Initiating gateway mode session: {:?}",
1537 session_name.unwrap_or("(auto)")
1538 );
1539
1540 let cmd = match session_name {
1542 Some(name) => TmuxSession::create_or_attach_command(name),
1543 None => TmuxSession::create_new_command(None),
1544 };
1545
1546 let gateway_tab_id = self
1548 .tab_manager
1549 .active_tab_id()
1550 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1551
1552 let tab = self
1553 .tab_manager
1554 .active_tab_mut()
1555 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1556
1557 if let Ok(term) = tab.terminal.try_lock() {
1559 crate::debug_info!(
1560 "TMUX",
1561 "Writing gateway command to tab {}: {}",
1562 gateway_tab_id,
1563 cmd.trim()
1564 );
1565 term.write(cmd.as_bytes())?;
1566 term.set_tmux_control_mode(true);
1568 crate::debug_info!(
1569 "TMUX",
1570 "Enabled tmux control mode parsing on tab {}",
1571 gateway_tab_id
1572 );
1573 } else {
1574 anyhow::bail!("Could not acquire terminal lock");
1575 }
1576
1577 tab.tmux_gateway_active = true;
1579
1580 self.tmux_gateway_tab_id = Some(gateway_tab_id);
1582 crate::debug_info!(
1583 "TMUX",
1584 "Gateway tab set to {}, state: Initiating",
1585 gateway_tab_id
1586 );
1587
1588 let mut session = TmuxSession::new();
1590 session.set_gateway_initiating();
1591 self.tmux_session = Some(session);
1592
1593 self.show_toast("tmux: Connecting...");
1595
1596 Ok(())
1597 }
1598
1599 pub fn attach_tmux_gateway(&mut self, session_name: &str) -> anyhow::Result<()> {
1603 if !self.config.tmux_enabled {
1604 anyhow::bail!("tmux integration is disabled");
1605 }
1606
1607 if self.tmux_session.is_some() && self.is_tmux_connected() {
1608 anyhow::bail!("Already connected to a tmux session");
1609 }
1610
1611 crate::debug_info!("TMUX", "Attaching to session via gateway: {}", session_name);
1612
1613 let cmd = TmuxSession::create_attach_command(session_name);
1615
1616 let gateway_tab_id = self
1618 .tab_manager
1619 .active_tab_id()
1620 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1621
1622 let tab = self
1623 .tab_manager
1624 .active_tab_mut()
1625 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1626
1627 if let Ok(term) = tab.terminal.try_lock() {
1629 crate::debug_info!(
1630 "TMUX",
1631 "Writing attach command to tab {}: {}",
1632 gateway_tab_id,
1633 cmd.trim()
1634 );
1635 term.write(cmd.as_bytes())?;
1636 term.set_tmux_control_mode(true);
1637 crate::debug_info!(
1638 "TMUX",
1639 "Enabled tmux control mode parsing on tab {}",
1640 gateway_tab_id
1641 );
1642 } else {
1643 anyhow::bail!("Could not acquire terminal lock");
1644 }
1645
1646 tab.tmux_gateway_active = true;
1648
1649 self.tmux_gateway_tab_id = Some(gateway_tab_id);
1651 crate::debug_info!(
1652 "TMUX",
1653 "Gateway tab set to {}, state: Initiating",
1654 gateway_tab_id
1655 );
1656
1657 let mut session = TmuxSession::new();
1659 session.set_gateway_initiating();
1660 self.tmux_session = Some(session);
1661
1662 self.show_toast(format!("tmux: Attaching to '{}'...", session_name));
1664
1665 Ok(())
1666 }
1667
1668 pub fn disconnect_tmux_session(&mut self) {
1670 self.tmux_gateway_tab_id = None;
1672
1673 for tab in self.tab_manager.tabs_mut() {
1675 if tab.tmux_gateway_active {
1676 tab.tmux_gateway_active = false;
1677 if let Ok(term) = tab.terminal.try_lock() {
1678 term.set_tmux_control_mode(false);
1679 }
1680 }
1681 }
1682
1683 if let Some(mut session) = self.tmux_session.take() {
1684 crate::debug_info!("TMUX", "Disconnecting from tmux session");
1685 session.disconnect();
1686 }
1687
1688 self.tmux_session_name = None;
1690
1691 self.tmux_sync = crate::tmux::TmuxSync::new();
1693
1694 self.update_window_title_with_tmux();
1696 }
1697
1698 pub fn is_tmux_connected(&self) -> bool {
1700 self.tmux_session
1701 .as_ref()
1702 .is_some_and(|s| s.state() == SessionState::Connected)
1703 }
1704
1705 pub fn is_gateway_active(&self) -> bool {
1707 self.tmux_session
1708 .as_ref()
1709 .is_some_and(|s| s.is_gateway_active())
1710 }
1711
1712 pub fn set_tmux_focused_pane_from_native(&mut self, native_pane_id: crate::pane::PaneId) {
1717 if let Some(tmux_pane_id) = self.native_pane_to_tmux_pane.get(&native_pane_id)
1718 && let Some(session) = &mut self.tmux_session
1719 {
1720 crate::debug_info!(
1721 "TMUX",
1722 "Setting focused pane: native {} -> tmux %{}",
1723 native_pane_id,
1724 tmux_pane_id
1725 );
1726 session.set_focused_pane(Some(*tmux_pane_id));
1727 }
1728 }
1729
1730 fn write_to_gateway(&self, cmd: &str) -> bool {
1739 let gateway_tab_id = match self.tmux_gateway_tab_id {
1740 Some(id) => id,
1741 None => {
1742 crate::debug_trace!("TMUX", "No gateway tab ID set");
1743 return false;
1744 }
1745 };
1746
1747 if let Some(tab) = self.tab_manager.get_tab(gateway_tab_id)
1748 && tab.tmux_gateway_active
1749 && let Ok(term) = tab.terminal.try_lock()
1750 && term.write(cmd.as_bytes()).is_ok()
1751 {
1752 return true;
1753 }
1754
1755 crate::debug_trace!("TMUX", "Failed to write to gateway tab");
1756 false
1757 }
1758
1759 pub fn send_input_via_tmux(&self, data: &[u8]) -> bool {
1766 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1768 crate::debug_trace!(
1769 "TMUX",
1770 "send_input_via_tmux: not sending - enabled={}, connected={}",
1771 self.config.tmux_enabled,
1772 self.is_tmux_connected()
1773 );
1774 return false;
1775 }
1776
1777 let session = match &self.tmux_session {
1778 Some(s) => s,
1779 None => return false,
1780 };
1781
1782 let cmd = match session.format_send_keys(data) {
1784 Some(c) => {
1785 crate::debug_trace!("TMUX", "Using pane-specific send-keys: {}", c.trim());
1786 c
1787 }
1788 None => {
1789 crate::debug_trace!("TMUX", "No focused pane for send-keys, trying window-based");
1790 if let Some(cmd) = self.format_send_keys_for_window(data) {
1792 crate::debug_trace!("TMUX", "Using window-based send-keys: {}", cmd.trim());
1793 cmd
1794 } else {
1795 let escaped = crate::tmux::escape_keys_for_tmux(data);
1798 format!("send-keys {}\n", escaped)
1799 }
1800 }
1801 };
1802
1803 if self.write_to_gateway(&cmd) {
1805 crate::debug_trace!("TMUX", "Sent {} bytes via gateway send-keys", data.len());
1806 return true;
1807 }
1808
1809 false
1810 }
1811
1812 fn format_send_keys_for_window(&self, data: &[u8]) -> Option<String> {
1814 let active_tab_id = self.tab_manager.active_tab_id()?;
1815
1816 let tmux_window_id = self.tmux_sync.get_window(active_tab_id)?;
1818
1819 let escaped = crate::tmux::escape_keys_for_tmux(data);
1821 Some(format!("send-keys -t @{} {}\n", tmux_window_id, escaped))
1822 }
1823
1824 #[allow(dead_code)]
1826 fn send_input_via_tmux_window(&self, data: &[u8]) -> bool {
1827 let active_tab_id = match self.tab_manager.active_tab_id() {
1828 Some(id) => id,
1829 None => return false,
1830 };
1831
1832 let tmux_window_id = match self.tmux_sync.get_window(active_tab_id) {
1834 Some(id) => id,
1835 None => {
1836 crate::debug_trace!(
1837 "TMUX",
1838 "No tmux window mapping for tab {}, using untargeted send-keys",
1839 active_tab_id
1840 );
1841 return false;
1842 }
1843 };
1844
1845 let escaped = crate::tmux::escape_keys_for_tmux(data);
1847 let cmd = format!("send-keys -t @{} {}\n", tmux_window_id, escaped);
1848
1849 if self.write_to_gateway(&cmd) {
1851 crate::debug_trace!(
1852 "TMUX",
1853 "Sent {} bytes via gateway to window @{}",
1854 data.len(),
1855 tmux_window_id
1856 );
1857 return true;
1858 }
1859
1860 false
1861 }
1862
1863 pub fn paste_via_tmux(&self, text: &str) -> bool {
1867 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1868 return false;
1869 }
1870
1871 let session = match &self.tmux_session {
1872 Some(s) => s,
1873 None => return false,
1874 };
1875
1876 let cmd = match session.format_send_literal(text) {
1878 Some(c) => c,
1879 None => return false,
1880 };
1881
1882 if self.write_to_gateway(&cmd) {
1884 crate::debug_info!("TMUX", "Pasted {} chars via gateway", text.len());
1885 return true;
1886 }
1887
1888 false
1889 }
1890
1891 pub fn split_pane_via_tmux(&self, vertical: bool) -> bool {
1900 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1901 return false;
1902 }
1903
1904 let session = match &self.tmux_session {
1905 Some(s) => s,
1906 None => return false,
1907 };
1908
1909 let pane_id = session.focused_pane();
1911
1912 let cmd = if vertical {
1914 match pane_id {
1915 Some(id) => format!("split-window -h -t %{}\n", id),
1916 None => "split-window -h\n".to_string(),
1917 }
1918 } else {
1919 match pane_id {
1920 Some(id) => format!("split-window -v -t %{}\n", id),
1921 None => "split-window -v\n".to_string(),
1922 }
1923 };
1924
1925 if self.write_to_gateway(&cmd) {
1927 crate::debug_info!(
1928 "TMUX",
1929 "Sent {} split command via gateway",
1930 if vertical { "vertical" } else { "horizontal" }
1931 );
1932 return true;
1933 }
1934
1935 false
1936 }
1937
1938 pub fn close_pane_via_tmux(&self) -> bool {
1944 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1945 return false;
1946 }
1947
1948 let session = match &self.tmux_session {
1949 Some(s) => s,
1950 None => return false,
1951 };
1952
1953 let pane_id = match session.focused_pane() {
1955 Some(id) => id,
1956 None => {
1957 crate::debug_info!("TMUX", "No focused pane to close");
1958 return false;
1959 }
1960 };
1961
1962 let cmd = format!("kill-pane -t %{}\n", pane_id);
1963
1964 if self.write_to_gateway(&cmd) {
1966 crate::debug_info!("TMUX", "Sent kill-pane command for pane %{}", pane_id);
1967 return true;
1968 }
1969
1970 false
1971 }
1972
1973 pub fn sync_clipboard_to_tmux(&self, content: &str) -> bool {
1979 if !self.config.tmux_clipboard_sync {
1981 return false;
1982 }
1983
1984 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1985 return false;
1986 }
1987
1988 if content.is_empty() {
1990 return false;
1991 }
1992
1993 let escaped = content.replace('\'', "'\\''");
1995 let cmd = format!("set-buffer '{}'\n", escaped);
1996
1997 if self.write_to_gateway(&cmd) {
1999 crate::debug_trace!(
2000 "TMUX",
2001 "Synced {} chars to tmux paste buffer",
2002 content.len()
2003 );
2004 return true;
2005 }
2006
2007 false
2008 }
2009
2010 pub fn sync_pane_resize_to_tmux(&self, is_horizontal_divider: bool) {
2023 if !self.is_gateway_active() {
2025 return;
2026 }
2027
2028 let (cell_width, cell_height) = match &self.renderer {
2030 Some(r) => (r.cell_width(), r.cell_height()),
2031 None => return,
2032 };
2033
2034 let pane_sizes: Vec<(crate::tmux::TmuxPaneId, usize, usize)> = if let Some(tab) =
2036 self.tab_manager.active_tab()
2037 && let Some(pm) = tab.pane_manager()
2038 {
2039 pm.all_panes()
2040 .iter()
2041 .filter_map(|pane| {
2042 let tmux_pane_id = self.native_pane_to_tmux_pane.get(&pane.id)?;
2044 let cols = (pane.bounds.width / cell_width).floor() as usize;
2046 let rows = (pane.bounds.height / cell_height).floor() as usize;
2047 Some((*tmux_pane_id, cols.max(1), rows.max(1)))
2048 })
2049 .collect()
2050 } else {
2051 return;
2052 };
2053
2054 for (tmux_pane_id, cols, rows) in pane_sizes {
2058 let cmd = if is_horizontal_divider {
2059 format!("resize-pane -t %{} -y {}\n", tmux_pane_id, rows)
2060 } else {
2061 format!("resize-pane -t %{} -x {}\n", tmux_pane_id, cols)
2062 };
2063 if self.write_to_gateway(&cmd) {
2064 crate::debug_info!(
2065 "TMUX",
2066 "Synced pane %{} {} resize to {}",
2067 tmux_pane_id,
2068 if is_horizontal_divider {
2069 "height"
2070 } else {
2071 "width"
2072 },
2073 if is_horizontal_divider { rows } else { cols }
2074 );
2075 }
2076 }
2077 }
2078
2079 fn apply_tmux_session_profile(&mut self, session_name: &str) {
2093 if let Some(ref profile_name) = self.config.tmux_profile {
2095 if let Some(profile) = self.profile_manager.find_by_name(profile_name) {
2096 let profile_id = profile.id;
2097 let profile_display = profile.name.clone();
2098 crate::debug_info!(
2099 "TMUX",
2100 "Applying configured tmux_profile '{}' for session '{}'",
2101 profile_display,
2102 session_name
2103 );
2104 self.apply_profile_to_gateway_tab(profile_id, &profile_display);
2105 return;
2106 } else {
2107 crate::debug_info!(
2108 "TMUX",
2109 "Configured tmux_profile '{}' not found",
2110 profile_name
2111 );
2112 }
2113 }
2114
2115 if let Some(profile) = self.profile_manager.find_by_tmux_session(session_name) {
2117 let profile_id = profile.id;
2118 let profile_display = profile.name.clone();
2119 crate::debug_info!(
2120 "TMUX",
2121 "Auto-switching to profile '{}' for tmux session '{}'",
2122 profile_display,
2123 session_name
2124 );
2125 self.apply_profile_to_gateway_tab(profile_id, &profile_display);
2126 } else {
2127 crate::debug_info!(
2128 "TMUX",
2129 "No profile matches tmux session '{}' - consider adding tmux_session_patterns to a profile",
2130 session_name
2131 );
2132 }
2133 }
2134
2135 fn apply_profile_to_gateway_tab(
2137 &mut self,
2138 profile_id: crate::profile::ProfileId,
2139 profile_name: &str,
2140 ) {
2141 let profile_settings = self.profile_manager.get(&profile_id).map(|p| {
2143 (
2144 p.tab_name.clone(),
2145 p.icon.clone(),
2146 p.badge_text.clone(),
2147 p.command.clone(),
2148 p.command_args.clone(),
2149 )
2150 });
2151
2152 if let Some(gateway_tab_id) = self.tmux_gateway_tab_id
2153 && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
2154 {
2155 tab.auto_applied_profile_id = Some(profile_id);
2157
2158 if let Some((tab_name, icon, badge_text, command, command_args)) = profile_settings {
2159 tab.profile_icon = icon;
2161
2162 if tab.pre_profile_title.is_none() {
2164 tab.pre_profile_title = Some(tab.title.clone());
2165 }
2166 tab.title = tab_name.unwrap_or_else(|| profile_name.to_string());
2168
2169 if let Some(badge_text) = badge_text {
2171 tab.badge_override = Some(badge_text.clone());
2172 crate::debug_info!(
2173 "TMUX",
2174 "Applied badge text '{}' from profile '{}'",
2175 badge_text,
2176 profile_name
2177 );
2178 }
2179
2180 if let Some(cmd) = command {
2182 let mut full_cmd = cmd;
2183 if let Some(args) = command_args {
2184 for arg in args {
2185 full_cmd.push(' ');
2186 full_cmd.push_str(&arg);
2187 }
2188 }
2189 full_cmd.push('\n');
2190
2191 let terminal_clone = std::sync::Arc::clone(&tab.terminal);
2192 self.runtime.spawn(async move {
2193 let term = terminal_clone.lock().await;
2194 if let Err(e) = term.write(full_cmd.as_bytes()) {
2195 log::error!("Failed to execute tmux profile command: {}", e);
2196 }
2197 });
2198 }
2199 }
2200
2201 self.show_toast(format!("tmux: Profile '{}' applied", profile_name));
2203 log::info!(
2204 "Applied profile '{}' for tmux session (gateway tab {})",
2205 profile_name,
2206 gateway_tab_id
2207 );
2208 }
2209
2210 if let Some(profile) = self.profile_manager.get(&profile_id) {
2212 let profile_clone = profile.clone();
2213 self.apply_profile_badge(&profile_clone);
2214 }
2215 }
2216
2217 pub fn handle_tmux_prefix_key(&mut self, event: &winit::event::KeyEvent) -> bool {
2224 if event.state != winit::event::ElementState::Pressed {
2226 return false;
2227 }
2228
2229 if !self.config.tmux_enabled || !self.is_tmux_connected() {
2231 return false;
2232 }
2233
2234 let modifiers = self.input_handler.modifiers.state();
2235
2236 if self.tmux_prefix_state.is_active() {
2238 use winit::keyboard::{Key, NamedKey};
2241 let is_modifier_only = matches!(
2242 event.logical_key,
2243 Key::Named(
2244 NamedKey::Shift
2245 | NamedKey::Control
2246 | NamedKey::Alt
2247 | NamedKey::Super
2248 | NamedKey::Meta
2249 )
2250 );
2251 if is_modifier_only {
2252 crate::debug_trace!(
2253 "TMUX",
2254 "Ignoring modifier-only key in prefix mode: {:?}",
2255 event.logical_key
2256 );
2257 return false; }
2259
2260 self.tmux_prefix_state.exit();
2262
2263 let focused_pane = self.tmux_session.as_ref().and_then(|s| s.focused_pane());
2265
2266 if let Some(cmd) =
2268 crate::tmux::translate_command_key(&event.logical_key, modifiers, focused_pane)
2269 {
2270 crate::debug_info!(
2271 "TMUX",
2272 "Prefix command: {:?} -> {}",
2273 event.logical_key,
2274 cmd.trim()
2275 );
2276
2277 if self.write_to_gateway(&cmd) {
2279 let cmd_base = cmd.split(" -t").next().unwrap_or(&cmd).trim();
2281 match cmd_base {
2282 "detach-client" => self.show_toast("tmux: Detaching..."),
2283 "new-window" => self.show_toast("tmux: New window"),
2284 _ => {}
2285 }
2286 return true;
2287 }
2288 } else {
2289 crate::debug_info!(
2291 "TMUX",
2292 "Unknown prefix command key: {:?}",
2293 event.logical_key
2294 );
2295 self.show_toast(format!(
2296 "tmux: Unknown command key: {:?}",
2297 event.logical_key
2298 ));
2299 }
2300 return true; }
2302
2303 if let Some(ref prefix_key) = self.tmux_prefix_key
2305 && prefix_key.matches(&event.logical_key, modifiers)
2306 {
2307 crate::debug_info!("TMUX", "Prefix key pressed, entering prefix mode");
2308 self.tmux_prefix_state.enter();
2309 self.show_toast("tmux: prefix...");
2310 return true;
2311 }
2312
2313 false
2314 }
2315}