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 content_width = size.width as f32 - padding * 2.0 - content_inset_right;
644 let content_height =
645 size.height as f32 - content_offset_y - padding - status_bar_height;
646 let bounds = crate::pane::PaneBounds::new(
647 padding,
648 content_offset_y,
649 content_width,
650 content_height,
651 );
652 pm.set_bounds(bounds);
653 crate::debug_info!(
654 "TMUX",
655 "Set pane manager bounds: {}x{} at ({}, {})",
656 content_width,
657 content_height,
658 padding,
659 content_offset_y
660 );
661 }
662
663 let existing_tmux_ids: std::collections::HashSet<_> =
666 self.tmux_pane_to_native_pane.keys().copied().collect();
667 let new_tmux_ids: std::collections::HashSet<_> = pane_ids.iter().copied().collect();
668
669 if existing_tmux_ids == new_tmux_ids && !existing_tmux_ids.is_empty() {
670 crate::debug_info!(
672 "TMUX",
673 "Layout change with same panes ({:?}) - preserving terminals, updating layout",
674 pane_ids
675 );
676
677 if let Some(pm) = tab.pane_manager_mut() {
679 pm.update_layout_from_tmux(&parsed_layout, &self.tmux_pane_to_native_pane);
681 pm.recalculate_bounds();
682
683 if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info {
686 pm.resize_all_terminals(cell_width, cell_height);
687 }
688 }
689
690 self.needs_redraw = true;
691 return; }
693
694 let panes_to_keep: std::collections::HashSet<_> = existing_tmux_ids
697 .intersection(&new_tmux_ids)
698 .copied()
699 .collect();
700 let panes_to_remove: Vec<_> = existing_tmux_ids
701 .difference(&new_tmux_ids)
702 .copied()
703 .collect();
704 let panes_to_add: Vec<_> = new_tmux_ids
705 .difference(&existing_tmux_ids)
706 .copied()
707 .collect();
708
709 if !panes_to_keep.is_empty()
711 && !panes_to_remove.is_empty()
712 && panes_to_add.is_empty()
713 {
714 crate::debug_info!(
715 "TMUX",
716 "Layout change: keeping {:?}, removing {:?}",
717 panes_to_keep,
718 panes_to_remove
719 );
720
721 let current_focused = self.tmux_session.as_ref().and_then(|s| s.focused_pane());
723 let focused_pane_removed = current_focused
724 .map(|fp| panes_to_remove.contains(&fp))
725 .unwrap_or(false);
726
727 if let Some(pm) = tab.pane_manager_mut() {
729 for tmux_pane_id in &panes_to_remove {
730 if let Some(native_pane_id) =
731 self.tmux_pane_to_native_pane.get(tmux_pane_id)
732 {
733 crate::debug_info!(
734 "TMUX",
735 "Removing native pane {} for closed tmux pane %{}",
736 native_pane_id,
737 tmux_pane_id
738 );
739 pm.close_pane(*native_pane_id);
740 }
741 }
742
743 let kept_mappings: std::collections::HashMap<_, _> = self
746 .tmux_pane_to_native_pane
747 .iter()
748 .filter(|(tmux_id, _)| panes_to_keep.contains(tmux_id))
749 .map(|(k, v)| (*k, *v))
750 .collect();
751
752 pm.update_layout_from_tmux(&parsed_layout, &kept_mappings);
753 pm.recalculate_bounds();
754
755 if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info {
757 pm.resize_all_terminals(cell_width, cell_height);
758 }
759 }
760
761 for tmux_pane_id in &panes_to_remove {
763 if let Some(native_id) = self.tmux_pane_to_native_pane.remove(tmux_pane_id)
764 {
765 self.native_pane_to_tmux_pane.remove(&native_id);
766 }
767 }
768
769 if focused_pane_removed
771 && let Some(new_focus) = panes_to_keep.iter().next().copied()
772 {
773 crate::debug_info!(
774 "TMUX",
775 "Focused pane was removed, updating tmux session focus to %{}",
776 new_focus
777 );
778 if let Some(session) = &mut self.tmux_session {
779 session.set_focused_pane(Some(new_focus));
780 }
781 }
782
783 crate::debug_info!(
784 "TMUX",
785 "After pane removal, mappings: {:?}",
786 self.tmux_pane_to_native_pane
787 );
788
789 self.needs_redraw = true;
790 self.request_redraw();
791 return; }
793
794 if !panes_to_keep.is_empty()
796 && !panes_to_add.is_empty()
797 && panes_to_remove.is_empty()
798 {
799 crate::debug_info!(
800 "TMUX",
801 "Layout change: keeping {:?}, adding {:?}",
802 panes_to_keep,
803 panes_to_add
804 );
805
806 if let Some(pm) = tab.pane_manager_mut() {
809 let existing_mappings: std::collections::HashMap<_, _> = panes_to_keep
811 .iter()
812 .filter_map(|tmux_id| {
813 self.tmux_pane_to_native_pane
814 .get(tmux_id)
815 .map(|native_id| (*tmux_id, *native_id))
816 })
817 .collect();
818
819 match pm.rebuild_from_tmux_layout(
820 &parsed_layout,
821 &existing_mappings,
822 &panes_to_add,
823 &self.config,
824 std::sync::Arc::clone(&self.runtime),
825 ) {
826 Ok(new_mappings) => {
827 self.tmux_pane_to_native_pane = new_mappings.clone();
829 self.native_pane_to_tmux_pane = new_mappings
830 .iter()
831 .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
832 .collect();
833
834 crate::debug_info!(
835 "TMUX",
836 "Rebuilt layout with {} panes: {:?}",
837 new_mappings.len(),
838 new_mappings
839 );
840
841 if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info
843 {
844 pm.resize_all_terminals(cell_width, cell_height);
845 }
846 }
847 Err(e) => {
848 crate::debug_error!("TMUX", "Failed to rebuild layout: {}", e);
849 }
850 }
851 }
852
853 self.request_pane_refresh(&panes_to_add);
855
856 crate::debug_info!(
857 "TMUX",
858 "After pane addition, mappings: {:?}",
859 self.tmux_pane_to_native_pane
860 );
861
862 self.needs_redraw = true;
863 self.request_redraw();
864 return; }
866
867 if let Some(pm) = tab.pane_manager_mut() {
869 crate::debug_info!(
870 "TMUX",
871 "Full layout recreation: existing={:?}, new={:?}",
872 existing_tmux_ids,
873 new_tmux_ids
874 );
875
876 match pm.set_from_tmux_layout(
877 &parsed_layout,
878 &self.config,
879 std::sync::Arc::clone(&self.runtime),
880 ) {
881 Ok(pane_mappings) => {
882 crate::debug_info!(
884 "TMUX",
885 "Storing pane mappings: {:?}",
886 pane_mappings
887 );
888 self.tmux_pane_to_native_pane = pane_mappings.clone();
890 self.native_pane_to_tmux_pane = pane_mappings
891 .iter()
892 .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
893 .collect();
894
895 crate::debug_info!(
896 "TMUX",
897 "Applied tmux layout to tab {}: {} pane mappings created",
898 tab_id,
899 pane_mappings.len()
900 );
901
902 if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
904 tab.tmux_pane_id = Some(pane_ids[0]);
905 }
906
907 self.request_pane_refresh(&pane_ids);
910
911 self.needs_redraw = true;
912 }
913 Err(e) => {
914 crate::debug_error!(
915 "TMUX",
916 "Failed to apply tmux layout to tab {}: {}",
917 tab_id,
918 e
919 );
920 if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
922 tab.tmux_pane_id = Some(pane_ids[0]);
923 }
924 }
925 }
926 } else {
927 if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
929 tab.tmux_pane_id = Some(pane_ids[0]);
930 crate::debug_info!(
931 "TMUX",
932 "Set tab {} tmux_pane_id to %{} for output routing (no pane manager)",
933 tab_id,
934 pane_ids[0]
935 );
936 }
937 }
938 }
939 } else {
940 crate::debug_info!(
942 "TMUX",
943 "No tab mapping for window @{}, creating new tab for layout",
944 window_id
945 );
946
947 if self.config.max_tabs == 0 || self.tab_manager.tab_count() < self.config.max_tabs {
949 let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
950 match self.tab_manager.new_tab(
951 &self.config,
952 std::sync::Arc::clone(&self.runtime),
953 false,
954 grid_size,
955 ) {
956 Ok(new_tab_id) => {
957 crate::debug_info!(
958 "TMUX",
959 "Created tab {} for tmux window @{}",
960 new_tab_id,
961 window_id
962 );
963
964 self.tmux_sync.map_window(window_id, new_tab_id);
966
967 if let Some(tab) = self.tab_manager.get_tab_mut(new_tab_id) {
969 tab.init_pane_manager();
970 tab.set_title(&format!("tmux @{}", window_id));
971
972 if let Some(window) = &self.window {
974 tab.start_refresh_task(
975 std::sync::Arc::clone(&self.runtime),
976 std::sync::Arc::clone(window),
977 self.config.max_fps,
978 );
979 }
980
981 if let Some((
983 size,
984 padding,
985 content_offset_y,
986 content_inset_right,
987 _cell_width,
988 _cell_height,
989 status_bar_height,
990 )) = bounds_info
991 && let Some(pm) = tab.pane_manager_mut()
992 {
993 let content_width =
994 size.width as f32 - padding * 2.0 - content_inset_right;
995 let content_height = size.height as f32
996 - content_offset_y
997 - padding
998 - status_bar_height;
999 let bounds = crate::pane::PaneBounds::new(
1000 padding,
1001 content_offset_y,
1002 content_width,
1003 content_height,
1004 );
1005 pm.set_bounds(bounds);
1006 }
1007
1008 if let Some(pm) = tab.pane_manager_mut() {
1010 match pm.set_from_tmux_layout(
1011 &parsed_layout,
1012 &self.config,
1013 std::sync::Arc::clone(&self.runtime),
1014 ) {
1015 Ok(pane_mappings) => {
1016 crate::debug_info!(
1017 "TMUX",
1018 "Storing pane mappings for new tab: {:?}",
1019 pane_mappings
1020 );
1021 self.native_pane_to_tmux_pane = pane_mappings
1023 .iter()
1024 .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
1025 .collect();
1026 self.tmux_pane_to_native_pane = pane_mappings;
1027
1028 if !pane_ids.is_empty() {
1030 tab.tmux_pane_id = Some(pane_ids[0]);
1031 }
1032
1033 self.request_pane_refresh(&pane_ids);
1035
1036 self.needs_redraw = true;
1037 }
1038 Err(e) => {
1039 crate::debug_error!(
1040 "TMUX",
1041 "Failed to apply tmux layout to new tab: {}",
1042 e
1043 );
1044 }
1045 }
1046 }
1047 }
1048
1049 self.tab_manager.switch_to(new_tab_id);
1051 }
1052 Err(e) => {
1053 crate::debug_error!(
1054 "TMUX",
1055 "Failed to create tab for tmux window @{}: {}",
1056 window_id,
1057 e
1058 );
1059 }
1060 }
1061 }
1062 }
1063 }
1064
1065 fn log_layout_node(node: &crate::tmux::LayoutNode, depth: usize) {
1067 let indent = " ".repeat(depth);
1068 match node {
1069 crate::tmux::LayoutNode::Pane {
1070 id,
1071 width,
1072 height,
1073 x,
1074 y,
1075 } => {
1076 crate::debug_trace!(
1077 "TMUX",
1078 "{}Pane %{}: {}x{} at ({}, {})",
1079 indent,
1080 id,
1081 width,
1082 height,
1083 x,
1084 y
1085 );
1086 }
1087 crate::tmux::LayoutNode::VerticalSplit {
1088 width,
1089 height,
1090 x,
1091 y,
1092 children,
1093 } => {
1094 crate::debug_trace!(
1095 "TMUX",
1096 "{}VerticalSplit: {}x{} at ({}, {}) with {} children",
1097 indent,
1098 width,
1099 height,
1100 x,
1101 y,
1102 children.len()
1103 );
1104 for child in children {
1105 Self::log_layout_node(child, depth + 1);
1106 }
1107 }
1108 crate::tmux::LayoutNode::HorizontalSplit {
1109 width,
1110 height,
1111 x,
1112 y,
1113 children,
1114 } => {
1115 crate::debug_trace!(
1116 "TMUX",
1117 "{}HorizontalSplit: {}x{} at ({}, {}) with {} children",
1118 indent,
1119 width,
1120 height,
1121 x,
1122 y,
1123 children.len()
1124 );
1125 for child in children {
1126 Self::log_layout_node(child, depth + 1);
1127 }
1128 }
1129 }
1130 }
1131
1132 fn handle_tmux_output(&mut self, pane_id: crate::tmux::TmuxPaneId, data: &[u8]) {
1134 if data.is_empty() {
1135 return;
1136 }
1137
1138 crate::debug_trace!(
1139 "TMUX",
1140 "Output from pane %{}: {} bytes",
1141 pane_id,
1142 data.len()
1143 );
1144
1145 if data.len() <= 20 {
1147 crate::debug_trace!(
1148 "TMUX",
1149 "Output data: {:?} (hex: {:02x?})",
1150 String::from_utf8_lossy(data),
1151 data
1152 );
1153 }
1154
1155 if self.tmux_sync.buffer_output(pane_id, data) {
1157 crate::debug_trace!(
1158 "TMUX",
1159 "Buffered {} bytes for pane %{} (paused)",
1160 data.len(),
1161 pane_id
1162 );
1163 return;
1164 }
1165
1166 crate::debug_trace!("TMUX", "Pane mappings: {:?}", self.tmux_pane_to_native_pane);
1168
1169 let native_pane_id = self
1172 .tmux_pane_to_native_pane
1173 .get(&pane_id)
1174 .copied()
1175 .or_else(|| self.tmux_sync.get_native_pane(pane_id));
1176
1177 if let Some(native_pane_id) = native_pane_id {
1178 for tab in self.tab_manager.tabs_mut() {
1180 if let Some(pane_manager) = tab.pane_manager_mut()
1181 && let Some(pane) = pane_manager.get_pane_mut(native_pane_id)
1182 && let Ok(term) = pane.terminal.try_lock()
1183 {
1184 term.process_data(data);
1186 crate::debug_trace!(
1187 "TMUX",
1188 "Routed {} bytes to pane {} (tmux %{})",
1189 data.len(),
1190 native_pane_id,
1191 pane_id
1192 );
1193 return;
1194 }
1195 }
1196 }
1197
1198 for tab in self.tab_manager.tabs_mut() {
1201 if tab.tmux_pane_id == Some(pane_id)
1202 && let Ok(term) = tab.terminal.try_lock()
1203 {
1204 term.process_data(data);
1205 crate::debug_trace!(
1206 "TMUX",
1207 "Routed {} bytes to tab terminal (tmux %{})",
1208 data.len(),
1209 pane_id
1210 );
1211 return;
1212 }
1213 }
1214
1215 crate::debug_trace!(
1219 "TMUX",
1220 "No direct mapping for tmux pane %{}, looking for existing tmux tab",
1221 pane_id
1222 );
1223
1224 for tab in self.tab_manager.tabs_mut() {
1226 if tab.tmux_pane_id.is_some()
1227 && !tab.tmux_gateway_active
1228 && let Ok(term) = tab.terminal.try_lock()
1229 {
1230 term.process_data(data);
1231 crate::debug_trace!(
1232 "TMUX",
1233 "Routed {} bytes from pane %{} to existing tmux tab (pane %{:?})",
1234 data.len(),
1235 pane_id,
1236 tab.tmux_pane_id
1237 );
1238 return;
1239 }
1240 }
1241
1242 crate::debug_info!(
1244 "TMUX",
1245 "No existing tmux tab found, creating new tab for pane %{}",
1246 pane_id
1247 );
1248
1249 if self.tmux_gateway_tab_id.is_some() {
1252 if self.config.max_tabs == 0 || self.tab_manager.tab_count() < self.config.max_tabs {
1254 let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
1255 match self.tab_manager.new_tab(
1256 &self.config,
1257 std::sync::Arc::clone(&self.runtime),
1258 false,
1259 grid_size,
1260 ) {
1261 Ok(new_tab_id) => {
1262 crate::debug_info!(
1263 "TMUX",
1264 "Created tab {} for tmux pane %{}",
1265 new_tab_id,
1266 pane_id
1267 );
1268
1269 if let Some(session) = &mut self.tmux_session
1271 && session.focused_pane().is_none()
1272 {
1273 session.set_focused_pane(Some(pane_id));
1274 }
1275
1276 if let Some(tab) = self.tab_manager.get_tab_mut(new_tab_id) {
1278 tab.tmux_pane_id = Some(pane_id);
1280 tab.set_title(&format!("tmux %{}", pane_id));
1281
1282 if let Some(window) = &self.window {
1284 tab.start_refresh_task(
1285 std::sync::Arc::clone(&self.runtime),
1286 std::sync::Arc::clone(window),
1287 self.config.max_fps,
1288 );
1289 }
1290
1291 if let Ok(term) = tab.terminal.try_lock() {
1293 term.process_data(data);
1294 }
1295 }
1296
1297 self.tab_manager.switch_to(new_tab_id);
1299 }
1300 Err(e) => {
1301 crate::debug_error!(
1302 "TMUX",
1303 "Failed to create tab for tmux pane %{}: {}",
1304 pane_id,
1305 e
1306 );
1307 }
1308 }
1309 } else {
1310 crate::debug_error!(
1311 "TMUX",
1312 "Cannot create tab for tmux pane %{}: max tabs reached",
1313 pane_id
1314 );
1315 }
1316 }
1317 }
1318
1319 fn handle_tmux_pane_focus_changed(&mut self, tmux_pane_id: crate::tmux::TmuxPaneId) {
1321 crate::debug_info!("TMUX", "Pane focus changed to %{}", tmux_pane_id);
1322
1323 if let Some(session) = &mut self.tmux_session {
1325 session.set_focused_pane(Some(tmux_pane_id));
1326 }
1327
1328 if let Some(native_pane_id) = self.tmux_pane_to_native_pane.get(&tmux_pane_id) {
1330 if let Some(tab) = self.tab_manager.active_tab_mut()
1332 && let Some(pm) = tab.pane_manager_mut()
1333 {
1334 pm.focus_pane(*native_pane_id);
1335 crate::debug_info!(
1336 "TMUX",
1337 "Updated native pane focus: tmux %{} -> native {}",
1338 tmux_pane_id,
1339 native_pane_id
1340 );
1341 }
1342 }
1343 }
1344
1345 fn handle_tmux_session_ended(&mut self) {
1347 crate::debug_info!("TMUX", "Session ended");
1348
1349 let gateway_tab_id = self.tmux_gateway_tab_id;
1351 let tmux_tabs_to_close: Vec<crate::tab::TabId> = self
1352 .tab_manager
1353 .tabs()
1354 .iter()
1355 .filter_map(|tab| {
1356 if tab.tmux_pane_id.is_some() && Some(tab.id) != gateway_tab_id {
1359 Some(tab.id)
1360 } else {
1361 None
1362 }
1363 })
1364 .collect();
1365
1366 for tab_id in tmux_tabs_to_close {
1368 crate::debug_info!("TMUX", "Closing tmux display tab {}", tab_id);
1369 let _ = self.tab_manager.close_tab(tab_id);
1370 }
1371
1372 if let Some(gateway_tab_id) = self.tmux_gateway_tab_id
1374 && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
1375 && tab.tmux_gateway_active
1376 {
1377 tab.tmux_gateway_active = false;
1378 tab.tmux_pane_id = None;
1379 tab.clear_auto_profile(); if let Ok(term) = tab.terminal.try_lock() {
1381 term.set_tmux_control_mode(false);
1382 }
1383 }
1384 self.tmux_gateway_tab_id = None;
1385
1386 if let Some(mut session) = self.tmux_session.take() {
1388 session.disconnect();
1389 }
1390 self.tmux_session_name = None;
1391
1392 self.tmux_pane_to_native_pane.clear();
1394 self.native_pane_to_tmux_pane.clear();
1395
1396 self.update_window_title_with_tmux();
1398
1399 self.tmux_sync = crate::tmux::TmuxSync::new();
1401
1402 self.show_toast("tmux: Session ended");
1404 }
1405
1406 fn handle_tmux_error(&mut self, msg: &str) {
1408 crate::debug_error!("TMUX", "Error from tmux: {}", msg);
1409
1410 self.deliver_notification("tmux Error", msg);
1412 }
1413
1414 fn handle_tmux_pause(&mut self) {
1416 crate::debug_info!("TMUX", "Received pause notification - buffering output");
1417
1418 self.tmux_sync.pause();
1420
1421 self.show_toast("tmux: Output paused (slow connection)");
1423 }
1424
1425 fn handle_tmux_continue(&mut self) {
1427 crate::debug_info!("TMUX", "Received continue notification - resuming output");
1428
1429 let buffered = self.tmux_sync.resume();
1431
1432 for (tmux_pane_id, data) in buffered {
1434 if !data.is_empty() {
1435 crate::debug_info!(
1436 "TMUX",
1437 "Flushing {} buffered bytes to pane %{}",
1438 data.len(),
1439 tmux_pane_id
1440 );
1441
1442 if let Some(native_pane_id) = self.tmux_sync.get_native_pane(tmux_pane_id) {
1444 for tab in self.tab_manager.tabs_mut() {
1446 if let Some(pane_manager) = tab.pane_manager_mut()
1447 && let Some(pane) = pane_manager.get_pane_mut(native_pane_id)
1448 {
1449 if let Ok(term) = pane.terminal.try_lock() {
1450 term.process_data(&data);
1451 }
1452 break;
1453 }
1454 }
1455 }
1456 }
1457 }
1458
1459 self.show_toast("tmux: Output resumed");
1461 }
1462
1463 #[allow(dead_code)]
1465 fn process_sync_actions(&mut self, actions: Vec<SyncAction>) {
1466 for action in actions {
1467 match action {
1468 SyncAction::CreateTab { window_id } => {
1469 crate::debug_info!("TMUX", "Sync: Create tab for window @{}", window_id);
1470 }
1471 SyncAction::CloseTab { tab_id } => {
1472 crate::debug_info!("TMUX", "Sync: Close tab {}", tab_id);
1473 }
1474 SyncAction::RenameTab { tab_id, name } => {
1475 crate::debug_info!("TMUX", "Sync: Rename tab {} to '{}'", tab_id, name);
1476 }
1477 SyncAction::UpdateLayout { tab_id, layout: _ } => {
1478 crate::debug_info!("TMUX", "Sync: Update layout for tab {}", tab_id);
1479 }
1480 SyncAction::PaneOutput { pane_id, data } => {
1481 crate::debug_trace!(
1482 "TMUX",
1483 "Sync: Route {} bytes to pane {}",
1484 data.len(),
1485 pane_id
1486 );
1487 }
1488 SyncAction::SessionEnded => {
1489 crate::debug_info!("TMUX", "Sync: Session ended");
1490 self.handle_tmux_session_ended();
1491 }
1492 SyncAction::Pause => {
1493 crate::debug_info!("TMUX", "Sync: Pause");
1494 self.handle_tmux_pause();
1495 }
1496 SyncAction::Continue => {
1497 crate::debug_info!("TMUX", "Sync: Continue");
1498 self.handle_tmux_continue();
1499 }
1500 }
1501 }
1502 }
1503
1504 pub fn initiate_tmux_gateway(&mut self, session_name: Option<&str>) -> anyhow::Result<()> {
1517 if !self.config.tmux_enabled {
1518 anyhow::bail!("tmux integration is disabled");
1519 }
1520
1521 if self.tmux_session.is_some() && self.is_tmux_connected() {
1522 anyhow::bail!("Already connected to a tmux session");
1523 }
1524
1525 crate::debug_info!(
1526 "TMUX",
1527 "Initiating gateway mode session: {:?}",
1528 session_name.unwrap_or("(auto)")
1529 );
1530
1531 let cmd = match session_name {
1533 Some(name) => TmuxSession::create_or_attach_command(name),
1534 None => TmuxSession::create_new_command(None),
1535 };
1536
1537 let gateway_tab_id = self
1539 .tab_manager
1540 .active_tab_id()
1541 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1542
1543 let tab = self
1544 .tab_manager
1545 .active_tab_mut()
1546 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1547
1548 if let Ok(term) = tab.terminal.try_lock() {
1550 crate::debug_info!(
1551 "TMUX",
1552 "Writing gateway command to tab {}: {}",
1553 gateway_tab_id,
1554 cmd.trim()
1555 );
1556 term.write(cmd.as_bytes())?;
1557 term.set_tmux_control_mode(true);
1559 crate::debug_info!(
1560 "TMUX",
1561 "Enabled tmux control mode parsing on tab {}",
1562 gateway_tab_id
1563 );
1564 } else {
1565 anyhow::bail!("Could not acquire terminal lock");
1566 }
1567
1568 tab.tmux_gateway_active = true;
1570
1571 self.tmux_gateway_tab_id = Some(gateway_tab_id);
1573 crate::debug_info!(
1574 "TMUX",
1575 "Gateway tab set to {}, state: Initiating",
1576 gateway_tab_id
1577 );
1578
1579 let mut session = TmuxSession::new();
1581 session.set_gateway_initiating();
1582 self.tmux_session = Some(session);
1583
1584 self.show_toast("tmux: Connecting...");
1586
1587 Ok(())
1588 }
1589
1590 pub fn attach_tmux_gateway(&mut self, session_name: &str) -> anyhow::Result<()> {
1594 if !self.config.tmux_enabled {
1595 anyhow::bail!("tmux integration is disabled");
1596 }
1597
1598 if self.tmux_session.is_some() && self.is_tmux_connected() {
1599 anyhow::bail!("Already connected to a tmux session");
1600 }
1601
1602 crate::debug_info!("TMUX", "Attaching to session via gateway: {}", session_name);
1603
1604 let cmd = TmuxSession::create_attach_command(session_name);
1606
1607 let gateway_tab_id = self
1609 .tab_manager
1610 .active_tab_id()
1611 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1612
1613 let tab = self
1614 .tab_manager
1615 .active_tab_mut()
1616 .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1617
1618 if let Ok(term) = tab.terminal.try_lock() {
1620 crate::debug_info!(
1621 "TMUX",
1622 "Writing attach command to tab {}: {}",
1623 gateway_tab_id,
1624 cmd.trim()
1625 );
1626 term.write(cmd.as_bytes())?;
1627 term.set_tmux_control_mode(true);
1628 crate::debug_info!(
1629 "TMUX",
1630 "Enabled tmux control mode parsing on tab {}",
1631 gateway_tab_id
1632 );
1633 } else {
1634 anyhow::bail!("Could not acquire terminal lock");
1635 }
1636
1637 tab.tmux_gateway_active = true;
1639
1640 self.tmux_gateway_tab_id = Some(gateway_tab_id);
1642 crate::debug_info!(
1643 "TMUX",
1644 "Gateway tab set to {}, state: Initiating",
1645 gateway_tab_id
1646 );
1647
1648 let mut session = TmuxSession::new();
1650 session.set_gateway_initiating();
1651 self.tmux_session = Some(session);
1652
1653 self.show_toast(format!("tmux: Attaching to '{}'...", session_name));
1655
1656 Ok(())
1657 }
1658
1659 pub fn disconnect_tmux_session(&mut self) {
1661 self.tmux_gateway_tab_id = None;
1663
1664 for tab in self.tab_manager.tabs_mut() {
1666 if tab.tmux_gateway_active {
1667 tab.tmux_gateway_active = false;
1668 if let Ok(term) = tab.terminal.try_lock() {
1669 term.set_tmux_control_mode(false);
1670 }
1671 }
1672 }
1673
1674 if let Some(mut session) = self.tmux_session.take() {
1675 crate::debug_info!("TMUX", "Disconnecting from tmux session");
1676 session.disconnect();
1677 }
1678
1679 self.tmux_session_name = None;
1681
1682 self.tmux_sync = crate::tmux::TmuxSync::new();
1684
1685 self.update_window_title_with_tmux();
1687 }
1688
1689 pub fn is_tmux_connected(&self) -> bool {
1691 self.tmux_session
1692 .as_ref()
1693 .is_some_and(|s| s.state() == SessionState::Connected)
1694 }
1695
1696 pub fn is_gateway_active(&self) -> bool {
1698 self.tmux_session
1699 .as_ref()
1700 .is_some_and(|s| s.is_gateway_active())
1701 }
1702
1703 pub fn set_tmux_focused_pane_from_native(&mut self, native_pane_id: crate::pane::PaneId) {
1708 if let Some(tmux_pane_id) = self.native_pane_to_tmux_pane.get(&native_pane_id)
1709 && let Some(session) = &mut self.tmux_session
1710 {
1711 crate::debug_info!(
1712 "TMUX",
1713 "Setting focused pane: native {} -> tmux %{}",
1714 native_pane_id,
1715 tmux_pane_id
1716 );
1717 session.set_focused_pane(Some(*tmux_pane_id));
1718 }
1719 }
1720
1721 fn write_to_gateway(&self, cmd: &str) -> bool {
1730 let gateway_tab_id = match self.tmux_gateway_tab_id {
1731 Some(id) => id,
1732 None => {
1733 crate::debug_trace!("TMUX", "No gateway tab ID set");
1734 return false;
1735 }
1736 };
1737
1738 if let Some(tab) = self.tab_manager.get_tab(gateway_tab_id)
1739 && tab.tmux_gateway_active
1740 && let Ok(term) = tab.terminal.try_lock()
1741 && term.write(cmd.as_bytes()).is_ok()
1742 {
1743 return true;
1744 }
1745
1746 crate::debug_trace!("TMUX", "Failed to write to gateway tab");
1747 false
1748 }
1749
1750 pub fn send_input_via_tmux(&self, data: &[u8]) -> bool {
1757 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1759 crate::debug_trace!(
1760 "TMUX",
1761 "send_input_via_tmux: not sending - enabled={}, connected={}",
1762 self.config.tmux_enabled,
1763 self.is_tmux_connected()
1764 );
1765 return false;
1766 }
1767
1768 let session = match &self.tmux_session {
1769 Some(s) => s,
1770 None => return false,
1771 };
1772
1773 let cmd = match session.format_send_keys(data) {
1775 Some(c) => {
1776 crate::debug_trace!("TMUX", "Using pane-specific send-keys: {}", c.trim());
1777 c
1778 }
1779 None => {
1780 crate::debug_trace!("TMUX", "No focused pane for send-keys, trying window-based");
1781 if let Some(cmd) = self.format_send_keys_for_window(data) {
1783 crate::debug_trace!("TMUX", "Using window-based send-keys: {}", cmd.trim());
1784 cmd
1785 } else {
1786 let escaped = crate::tmux::escape_keys_for_tmux(data);
1789 format!("send-keys {}\n", escaped)
1790 }
1791 }
1792 };
1793
1794 if self.write_to_gateway(&cmd) {
1796 crate::debug_trace!("TMUX", "Sent {} bytes via gateway send-keys", data.len());
1797 return true;
1798 }
1799
1800 false
1801 }
1802
1803 fn format_send_keys_for_window(&self, data: &[u8]) -> Option<String> {
1805 let active_tab_id = self.tab_manager.active_tab_id()?;
1806
1807 let tmux_window_id = self.tmux_sync.get_window(active_tab_id)?;
1809
1810 let escaped = crate::tmux::escape_keys_for_tmux(data);
1812 Some(format!("send-keys -t @{} {}\n", tmux_window_id, escaped))
1813 }
1814
1815 #[allow(dead_code)]
1817 fn send_input_via_tmux_window(&self, data: &[u8]) -> bool {
1818 let active_tab_id = match self.tab_manager.active_tab_id() {
1819 Some(id) => id,
1820 None => return false,
1821 };
1822
1823 let tmux_window_id = match self.tmux_sync.get_window(active_tab_id) {
1825 Some(id) => id,
1826 None => {
1827 crate::debug_trace!(
1828 "TMUX",
1829 "No tmux window mapping for tab {}, using untargeted send-keys",
1830 active_tab_id
1831 );
1832 return false;
1833 }
1834 };
1835
1836 let escaped = crate::tmux::escape_keys_for_tmux(data);
1838 let cmd = format!("send-keys -t @{} {}\n", tmux_window_id, escaped);
1839
1840 if self.write_to_gateway(&cmd) {
1842 crate::debug_trace!(
1843 "TMUX",
1844 "Sent {} bytes via gateway to window @{}",
1845 data.len(),
1846 tmux_window_id
1847 );
1848 return true;
1849 }
1850
1851 false
1852 }
1853
1854 pub fn paste_via_tmux(&self, text: &str) -> bool {
1858 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1859 return false;
1860 }
1861
1862 let session = match &self.tmux_session {
1863 Some(s) => s,
1864 None => return false,
1865 };
1866
1867 let cmd = match session.format_send_literal(text) {
1869 Some(c) => c,
1870 None => return false,
1871 };
1872
1873 if self.write_to_gateway(&cmd) {
1875 crate::debug_info!("TMUX", "Pasted {} chars via gateway", text.len());
1876 return true;
1877 }
1878
1879 false
1880 }
1881
1882 pub fn split_pane_via_tmux(&self, vertical: bool) -> bool {
1891 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1892 return false;
1893 }
1894
1895 let session = match &self.tmux_session {
1896 Some(s) => s,
1897 None => return false,
1898 };
1899
1900 let pane_id = session.focused_pane();
1902
1903 let cmd = if vertical {
1905 match pane_id {
1906 Some(id) => format!("split-window -h -t %{}\n", id),
1907 None => "split-window -h\n".to_string(),
1908 }
1909 } else {
1910 match pane_id {
1911 Some(id) => format!("split-window -v -t %{}\n", id),
1912 None => "split-window -v\n".to_string(),
1913 }
1914 };
1915
1916 if self.write_to_gateway(&cmd) {
1918 crate::debug_info!(
1919 "TMUX",
1920 "Sent {} split command via gateway",
1921 if vertical { "vertical" } else { "horizontal" }
1922 );
1923 return true;
1924 }
1925
1926 false
1927 }
1928
1929 pub fn close_pane_via_tmux(&self) -> bool {
1935 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1936 return false;
1937 }
1938
1939 let session = match &self.tmux_session {
1940 Some(s) => s,
1941 None => return false,
1942 };
1943
1944 let pane_id = match session.focused_pane() {
1946 Some(id) => id,
1947 None => {
1948 crate::debug_info!("TMUX", "No focused pane to close");
1949 return false;
1950 }
1951 };
1952
1953 let cmd = format!("kill-pane -t %{}\n", pane_id);
1954
1955 if self.write_to_gateway(&cmd) {
1957 crate::debug_info!("TMUX", "Sent kill-pane command for pane %{}", pane_id);
1958 return true;
1959 }
1960
1961 false
1962 }
1963
1964 pub fn sync_clipboard_to_tmux(&self, content: &str) -> bool {
1970 if !self.config.tmux_clipboard_sync {
1972 return false;
1973 }
1974
1975 if !self.config.tmux_enabled || !self.is_tmux_connected() {
1976 return false;
1977 }
1978
1979 if content.is_empty() {
1981 return false;
1982 }
1983
1984 let escaped = content.replace('\'', "'\\''");
1986 let cmd = format!("set-buffer '{}'\n", escaped);
1987
1988 if self.write_to_gateway(&cmd) {
1990 crate::debug_trace!(
1991 "TMUX",
1992 "Synced {} chars to tmux paste buffer",
1993 content.len()
1994 );
1995 return true;
1996 }
1997
1998 false
1999 }
2000
2001 pub fn sync_pane_resize_to_tmux(&self, is_horizontal_divider: bool) {
2014 if !self.is_gateway_active() {
2016 return;
2017 }
2018
2019 let (cell_width, cell_height) = match &self.renderer {
2021 Some(r) => (r.cell_width(), r.cell_height()),
2022 None => return,
2023 };
2024
2025 let pane_sizes: Vec<(crate::tmux::TmuxPaneId, usize, usize)> = if let Some(tab) =
2027 self.tab_manager.active_tab()
2028 && let Some(pm) = tab.pane_manager()
2029 {
2030 pm.all_panes()
2031 .iter()
2032 .filter_map(|pane| {
2033 let tmux_pane_id = self.native_pane_to_tmux_pane.get(&pane.id)?;
2035 let cols = (pane.bounds.width / cell_width).floor() as usize;
2037 let rows = (pane.bounds.height / cell_height).floor() as usize;
2038 Some((*tmux_pane_id, cols.max(1), rows.max(1)))
2039 })
2040 .collect()
2041 } else {
2042 return;
2043 };
2044
2045 for (tmux_pane_id, cols, rows) in pane_sizes {
2049 let cmd = if is_horizontal_divider {
2050 format!("resize-pane -t %{} -y {}\n", tmux_pane_id, rows)
2051 } else {
2052 format!("resize-pane -t %{} -x {}\n", tmux_pane_id, cols)
2053 };
2054 if self.write_to_gateway(&cmd) {
2055 crate::debug_info!(
2056 "TMUX",
2057 "Synced pane %{} {} resize to {}",
2058 tmux_pane_id,
2059 if is_horizontal_divider {
2060 "height"
2061 } else {
2062 "width"
2063 },
2064 if is_horizontal_divider { rows } else { cols }
2065 );
2066 }
2067 }
2068 }
2069
2070 fn apply_tmux_session_profile(&mut self, session_name: &str) {
2084 if let Some(ref profile_name) = self.config.tmux_profile {
2086 if let Some(profile) = self.profile_manager.find_by_name(profile_name) {
2087 let profile_id = profile.id;
2088 let profile_display = profile.name.clone();
2089 crate::debug_info!(
2090 "TMUX",
2091 "Applying configured tmux_profile '{}' for session '{}'",
2092 profile_display,
2093 session_name
2094 );
2095 self.apply_profile_to_gateway_tab(profile_id, &profile_display);
2096 return;
2097 } else {
2098 crate::debug_info!(
2099 "TMUX",
2100 "Configured tmux_profile '{}' not found",
2101 profile_name
2102 );
2103 }
2104 }
2105
2106 if let Some(profile) = self.profile_manager.find_by_tmux_session(session_name) {
2108 let profile_id = profile.id;
2109 let profile_display = profile.name.clone();
2110 crate::debug_info!(
2111 "TMUX",
2112 "Auto-switching to profile '{}' for tmux session '{}'",
2113 profile_display,
2114 session_name
2115 );
2116 self.apply_profile_to_gateway_tab(profile_id, &profile_display);
2117 } else {
2118 crate::debug_info!(
2119 "TMUX",
2120 "No profile matches tmux session '{}' - consider adding tmux_session_patterns to a profile",
2121 session_name
2122 );
2123 }
2124 }
2125
2126 fn apply_profile_to_gateway_tab(
2128 &mut self,
2129 profile_id: crate::profile::ProfileId,
2130 profile_name: &str,
2131 ) {
2132 let profile_settings = self.profile_manager.get(&profile_id).map(|p| {
2134 (
2135 p.tab_name.clone(),
2136 p.icon.clone(),
2137 p.badge_text.clone(),
2138 p.command.clone(),
2139 p.command_args.clone(),
2140 )
2141 });
2142
2143 if let Some(gateway_tab_id) = self.tmux_gateway_tab_id
2144 && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
2145 {
2146 tab.auto_applied_profile_id = Some(profile_id);
2148
2149 if let Some((tab_name, icon, badge_text, command, command_args)) = profile_settings {
2150 tab.profile_icon = icon;
2152
2153 if tab.pre_profile_title.is_none() {
2155 tab.pre_profile_title = Some(tab.title.clone());
2156 }
2157 tab.title = tab_name.unwrap_or_else(|| profile_name.to_string());
2159
2160 if let Some(badge_text) = badge_text {
2162 tab.badge_override = Some(badge_text.clone());
2163 crate::debug_info!(
2164 "TMUX",
2165 "Applied badge text '{}' from profile '{}'",
2166 badge_text,
2167 profile_name
2168 );
2169 }
2170
2171 if let Some(cmd) = command {
2173 let mut full_cmd = cmd;
2174 if let Some(args) = command_args {
2175 for arg in args {
2176 full_cmd.push(' ');
2177 full_cmd.push_str(&arg);
2178 }
2179 }
2180 full_cmd.push('\n');
2181
2182 let terminal_clone = std::sync::Arc::clone(&tab.terminal);
2183 self.runtime.spawn(async move {
2184 let term = terminal_clone.lock().await;
2185 if let Err(e) = term.write(full_cmd.as_bytes()) {
2186 log::error!("Failed to execute tmux profile command: {}", e);
2187 }
2188 });
2189 }
2190 }
2191
2192 self.show_toast(format!("tmux: Profile '{}' applied", profile_name));
2194 log::info!(
2195 "Applied profile '{}' for tmux session (gateway tab {})",
2196 profile_name,
2197 gateway_tab_id
2198 );
2199 }
2200
2201 if let Some(profile) = self.profile_manager.get(&profile_id) {
2203 let profile_clone = profile.clone();
2204 self.apply_profile_badge(&profile_clone);
2205 }
2206 }
2207
2208 pub fn handle_tmux_prefix_key(&mut self, event: &winit::event::KeyEvent) -> bool {
2215 if event.state != winit::event::ElementState::Pressed {
2217 return false;
2218 }
2219
2220 if !self.config.tmux_enabled || !self.is_tmux_connected() {
2222 return false;
2223 }
2224
2225 let modifiers = self.input_handler.modifiers.state();
2226
2227 if self.tmux_prefix_state.is_active() {
2229 use winit::keyboard::{Key, NamedKey};
2232 let is_modifier_only = matches!(
2233 event.logical_key,
2234 Key::Named(
2235 NamedKey::Shift
2236 | NamedKey::Control
2237 | NamedKey::Alt
2238 | NamedKey::Super
2239 | NamedKey::Meta
2240 )
2241 );
2242 if is_modifier_only {
2243 crate::debug_trace!(
2244 "TMUX",
2245 "Ignoring modifier-only key in prefix mode: {:?}",
2246 event.logical_key
2247 );
2248 return false; }
2250
2251 self.tmux_prefix_state.exit();
2253
2254 let focused_pane = self.tmux_session.as_ref().and_then(|s| s.focused_pane());
2256
2257 if let Some(cmd) =
2259 crate::tmux::translate_command_key(&event.logical_key, modifiers, focused_pane)
2260 {
2261 crate::debug_info!(
2262 "TMUX",
2263 "Prefix command: {:?} -> {}",
2264 event.logical_key,
2265 cmd.trim()
2266 );
2267
2268 if self.write_to_gateway(&cmd) {
2270 let cmd_base = cmd.split(" -t").next().unwrap_or(&cmd).trim();
2272 match cmd_base {
2273 "detach-client" => self.show_toast("tmux: Detaching..."),
2274 "new-window" => self.show_toast("tmux: New window"),
2275 _ => {}
2276 }
2277 return true;
2278 }
2279 } else {
2280 crate::debug_info!(
2282 "TMUX",
2283 "Unknown prefix command key: {:?}",
2284 event.logical_key
2285 );
2286 self.show_toast(format!(
2287 "tmux: Unknown command key: {:?}",
2288 event.logical_key
2289 ));
2290 }
2291 return true; }
2293
2294 if let Some(ref prefix_key) = self.tmux_prefix_key
2296 && prefix_key.matches(&event.logical_key, modifiers)
2297 {
2298 crate::debug_info!("TMUX", "Prefix key pressed, entering prefix mode");
2299 self.tmux_prefix_state.enter();
2300 self.show_toast("tmux: prefix...");
2301 return true;
2302 }
2303
2304 false
2305 }
2306}