Skip to main content

par_term/app/
tmux_handler.rs

1//! tmux notification handling for the application
2//!
3//! This module processes notifications received from tmux control mode
4//! and routes them to appropriate handlers.
5//!
6//! ## Gateway Mode
7//!
8//! Gateway mode writes `tmux -CC` commands to the existing terminal's PTY
9//! instead of spawning a separate process. This is the iTerm2 approach and
10//! provides reliable tmux integration.
11//!
12//! The flow is:
13//! 1. User selects "Create Session" in picker
14//! 2. We write `tmux -CC new-session -s name\n` to the active tab's PTY
15//! 3. Enable tmux control mode parsing in the terminal
16//! 4. Receive notifications via `%session-changed`, `%output`, etc.
17//! 5. Route input via `send-keys` commands back to the same PTY
18
19use crate::app::window_state::WindowState;
20use crate::tmux::{
21    ParserBridge, SessionState, SyncAction, TmuxLayout, TmuxNotification, TmuxSession, TmuxWindowId,
22};
23
24impl WindowState {
25    /// Poll and process tmux notifications from the control mode session.
26    ///
27    /// In gateway mode, notifications come from the terminal's tmux control parser
28    /// rather than a separate channel. This should be called in about_to_wait.
29    ///
30    /// Returns true if any notifications were processed (triggers redraw).
31    pub(crate) fn check_tmux_notifications(&mut self) -> bool {
32        // Early exit if tmux integration is disabled
33        if !self.config.tmux_enabled {
34            return false;
35        }
36
37        // Check if we have an active gateway session
38        let _session = match &self.tmux_session {
39            Some(s) if s.is_gateway_active() => s,
40            _ => return false,
41        };
42
43        // Get the gateway tab ID - this is where the tmux control connection lives
44        let gateway_tab_id = match self.tmux_gateway_tab_id {
45            Some(id) => id,
46            None => return false,
47        };
48
49        // Drain notifications from the gateway tab's terminal tmux parser
50        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        // Log all raw core notifications for debugging
65        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        // Convert core notifications to frontend notifications
76        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        // First, update gateway state based on notifications
94        for notification in &notifications {
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        // Process notifications in priority order:
110        // 1. Session/Window structure (setup)
111        // 2. LayoutChange (creates pane mappings)
112        // 3. Output (uses pane mappings)
113        // This ensures pane mappings exist before output arrives.
114
115        // Separate notifications by type for ordered processing
116        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 &notification {
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        // Process session/window structure first
148        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        // Process layout changes second (creates pane mappings)
182        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        // Process output third (uses pane mappings)
190        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        // Process other notifications last
198        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    /// Handle session started notification
222    fn handle_tmux_session_started(&mut self, session_name: &str) {
223        crate::debug_info!("TMUX", "Session started: {}", session_name);
224
225        // Store the session name for later use (e.g., window title updates)
226        self.tmux_session_name = Some(session_name.to_string());
227
228        // Update window title with session name: "par-term - [tmux: session_name]"
229        self.update_window_title_with_tmux();
230
231        // Check for automatic profile switching based on tmux session name
232        self.apply_tmux_session_profile(session_name);
233
234        // Update the gateway tab's title to show tmux session
235        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        // Enable sync now that session is connected
248        self.tmux_sync.enable();
249
250        // Note: tmux_gateway_active was already set on the gateway tab during initiate_tmux_gateway()
251
252        // Set window-size to 'smallest' so tmux respects par-term's size
253        // even when other (larger) clients are attached.
254        // This is critical for proper multi-client behavior.
255        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        // Tell tmux the terminal size so panes can be properly sized
262        // Without this, tmux uses a very small default and splits will fail
263        self.send_tmux_client_size();
264
265        // Note: Initial pane content comes from layout-change handling which sends Ctrl+L
266        // to each pane. We don't send Enter here as it would execute a command.
267
268        // Show success toast
269        self.show_toast(format!("tmux: Connected to session '{}'", session_name));
270    }
271
272    /// Send the terminal size to tmux so it knows the client dimensions
273    ///
274    /// In control mode, tmux doesn't know the terminal size unless we tell it.
275    /// Without this, tmux uses a very small default and pane splits will fail
276    /// with "no space for new pane".
277    fn send_tmux_client_size(&self) {
278        // Get the terminal grid size from the renderer
279        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    /// Notify tmux of a window/pane resize
295    ///
296    /// Called when the window is resized to keep tmux in sync with par-term's size.
297    /// This sends `refresh-client -C cols,rows` to tmux in gateway mode.
298    pub fn notify_tmux_of_resize(&self) {
299        // Only send if tmux gateway is active
300        if !self.is_gateway_active() {
301            return;
302        }
303
304        self.send_tmux_client_size();
305    }
306
307    /// Request content refresh for specific panes
308    ///
309    /// After learning about panes from a layout change, we need to trigger
310    /// each pane to send its content. tmux only sends %output for NEW content,
311    /// not existing screen content when attaching.
312    ///
313    /// We use two approaches:
314    /// 1. Send Ctrl+L (C-l) to each pane, which triggers shell screen redraw
315    /// 2. Use capture-pane -p to get the current pane content (comes as command response)
316    fn request_pane_refresh(&self, pane_ids: &[crate::tmux::TmuxPaneId]) {
317        for pane_id in pane_ids {
318            // Approach 1: Send Ctrl+L (screen redraw signal) to trigger shell to repaint
319            // This works for interactive shells that respond to SIGWINCH-like events
320            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        // Request client refresh which may help with layout sync
327        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    /// Update window title with tmux session info
338    /// Format: "window_title - [tmux: session_name]"
339    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    /// Handle session renamed notification
351    fn handle_tmux_session_renamed(&mut self, session_name: &str) {
352        crate::debug_info!("TMUX", "Session renamed to: {}", session_name);
353
354        // Update stored session name
355        self.tmux_session_name = Some(session_name.to_string());
356
357        // Update window title with new session name
358        self.update_window_title_with_tmux();
359    }
360
361    /// Handle window add notification - creates a new tab
362    fn handle_tmux_window_add(&mut self, window_id: TmuxWindowId) {
363        crate::debug_info!("TMUX", "Window added: @{}", window_id);
364
365        // Check max tabs limit
366        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        // Get current grid size from renderer
377        let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
378
379        // Create a new tab for this tmux window
380        match self.tab_manager.new_tab(
381            &self.config,
382            std::sync::Arc::clone(&self.runtime),
383            false, // Don't inherit CWD from active tab for tmux
384            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                // Register the mapping
395                self.tmux_sync.map_window(window_id, tab_id);
396
397                // Set initial title based on tmux window ID
398                // Note: These tabs are for displaying tmux windows, but the gateway tab
399                // is where the actual tmux control connection lives. We store the tmux pane ID
400                // on the tab so we know which pane to route input to.
401                if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
402                    tab.set_title(&format!("tmux @{}", window_id));
403                    // Note: Don't set tmux_gateway_active here - only the gateway tab is the gateway
404
405                    // Start refresh task for the new tab
406                    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                    // Resize terminal to match current renderer dimensions
415                    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    /// Handle window close notification - closes corresponding tab
450    fn handle_tmux_window_close(&mut self, window_id: TmuxWindowId) {
451        crate::debug_info!("TMUX", "Window closed: @{}", window_id);
452
453        // Find the corresponding tab
454        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            // Close the tab
463            let was_last = self.tab_manager.close_tab(tab_id);
464
465            // Remove the mapping
466            self.tmux_sync.unmap_window(window_id);
467
468            if was_last {
469                // Last tab closed - trigger session end handling
470                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    /// Handle window renamed notification
483    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        // Find the corresponding tab and update its title
487        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    /// Handle layout change notification - updates pane arrangement
502    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        // Parse the layout string
511        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        // Log the parsed layout structure
525        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        // Log the layout structure for debugging
535        Self::log_layout_node(&parsed_layout.root, 0);
536
537        // Update focused pane in session if we have one
538        if !pane_ids.is_empty()
539            && let Some(session) = &mut self.tmux_session
540        {
541            // Default to first pane if no focused pane set
542            if session.focused_pane().is_none() {
543                session.set_focused_pane(Some(pane_ids[0]));
544            }
545        }
546
547        // Find the corresponding tab and create window mapping if needed
548        let tab_id = if let Some(id) = self.tmux_sync.get_tab(window_id) {
549            Some(id)
550        } else {
551            // No window mapping exists - try to find a tab that has one of our panes
552            // This happens when we connect to an existing session and receive layout before window-add
553            let mut found_tab_id = None;
554            for pane_id in &pane_ids {
555                // Check if any tab has this tmux_pane_id set
556                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 we found a tab, create the window mapping
575            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        // Get bounds info from renderer for proper pane sizing (needed for both paths)
589        // Calculate status bar height for proper content area
590        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            // Scale status_bar_height from logical to physical pixels
603            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            // Apply the tmux layout to native pane rendering
627            if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
628                // Initialize pane manager if needed
629                tab.init_pane_manager();
630
631                // Set pane bounds before applying layout
632                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                // Check if we already have mappings for these exact tmux pane IDs
664                // If so, we should preserve the existing native panes/terminals
665                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                    // Same panes - preserve terminals but update layout structure
671                    crate::debug_info!(
672                        "TMUX",
673                        "Layout change with same panes ({:?}) - preserving terminals, updating layout",
674                        pane_ids
675                    );
676
677                    // Update the pane tree structure from the new layout without recreating terminals
678                    if let Some(pm) = tab.pane_manager_mut() {
679                        // Update layout structure (ratios, positions) from tmux layout
680                        pm.update_layout_from_tmux(&parsed_layout, &self.tmux_pane_to_native_pane);
681                        pm.recalculate_bounds();
682
683                        // Resize terminals to match new bounds
684                        // No padding in tmux mode - tmux controls the layout
685                        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; // Early return - don't recreate panes
692                }
693
694                // Check if new panes are a SUBSET of existing (panes were closed)
695                // or if there's overlap (some panes closed, some remain)
696                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 we have panes to keep and panes to remove, handle incrementally
710                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                    // Check if any of the removed panes was the focused pane
722                    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                    // Remove the closed panes from our native pane tree
728                    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                        // Update layout structure for remaining panes
744                        // Build new mappings with only the kept panes
745                        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                        // Resize terminals to match new bounds
756                        if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info {
757                            pm.resize_all_terminals(cell_width, cell_height);
758                        }
759                    }
760
761                    // Update mappings - remove closed panes
762                    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 the focused pane was removed, update tmux session focus to first remaining pane
770                    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; // Early return - don't recreate remaining panes
792                }
793
794                // Handle case where panes are ADDED (split) while keeping existing ones
795                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                    // Rebuild the entire tree structure from the tmux layout
807                    // This preserves existing pane terminals while creating correct structure
808                    if let Some(pm) = tab.pane_manager_mut() {
809                        // Create a mapping of tmux pane IDs to keep -> their native IDs
810                        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                                // Update our mappings with the new ones
828                                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                                // Resize terminals to match new bounds
842                                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                    // Request content for the new panes only
854                    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; // Early return - don't recreate all panes
865                }
866
867                // Full layout recreation needed (complete replacement or complex changes)
868                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                            // Store the pane mappings for output routing
883                            crate::debug_info!(
884                                "TMUX",
885                                "Storing pane mappings: {:?}",
886                                pane_mappings
887                            );
888                            // Store both forward and reverse mappings
889                            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                            // Set tab's tmux_pane_id to first pane for legacy output routing
903                            if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
904                                tab.tmux_pane_id = Some(pane_ids[0]);
905                            }
906
907                            // Request content refresh for all panes
908                            // tmux doesn't send existing content on attach
909                            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                            // Fall back to legacy routing
921                            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                    // No pane manager - use legacy routing
928                    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            // No tab mapping found - create a new tab for this tmux window
941            crate::debug_info!(
942                "TMUX",
943                "No tab mapping for window @{}, creating new tab for layout",
944                window_id
945            );
946
947            // Create a new tab for this tmux window
948            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                        // Register the window mapping
965                        self.tmux_sync.map_window(window_id, new_tab_id);
966
967                        // Now apply the layout to this tab
968                        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                            // Start refresh task
973                            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                            // Set pane bounds
982                            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                            // Apply the tmux layout
1009                            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                                        // Store both forward and reverse mappings
1022                                        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                                        // Set tab's tmux_pane_id to first pane
1029                                        if !pane_ids.is_empty() {
1030                                            tab.tmux_pane_id = Some(pane_ids[0]);
1031                                        }
1032
1033                                        // Request content refresh for all panes
1034                                        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                        // Switch to the new tab
1050                        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    /// Log a layout node and its children recursively for debugging
1066    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    /// Handle pane output notification - routes to correct terminal
1133    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        // Log first few bytes for debugging space issue
1146        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        // Check if output is paused - buffer if so
1156        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        // Debug: log the current mapping state
1167        crate::debug_trace!("TMUX", "Pane mappings: {:?}", self.tmux_pane_to_native_pane);
1168
1169        // First, try to find a native pane mapping (for split panes)
1170        // Check our direct mapping first, then fall back to tmux_sync
1171        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            // Find the pane across all tabs and route output to it
1179            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                    // Route the data to this pane's terminal
1185                    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        // No native pane mapping - check for tab-level tmux pane mapping
1199        // (This is used when we create tabs for tmux panes without split pane manager)
1200        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        // No direct mapping for this pane - try to find an existing tmux tab to route to
1216        // This handles the case where tmux has multiple panes but we don't have native
1217        // split pane rendering yet. Route all output to the first tmux-connected tab.
1218        crate::debug_trace!(
1219            "TMUX",
1220            "No direct mapping for tmux pane %{}, looking for existing tmux tab",
1221            pane_id
1222        );
1223
1224        // First, try to find any tab with a tmux_pane_id set (existing tmux display)
1225        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        // No existing tmux tab found - create one
1243        crate::debug_info!(
1244            "TMUX",
1245            "No existing tmux tab found, creating new tab for pane %{}",
1246            pane_id
1247        );
1248
1249        // Don't route to the gateway tab - that shows raw protocol
1250        // Instead, create a new tab for this tmux pane
1251        if self.tmux_gateway_tab_id.is_some() {
1252            // Check if we can create a new tab
1253            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                        // Set the focused pane if not already set
1270                        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                        // Configure the new tab for this tmux pane
1277                        if let Some(tab) = self.tab_manager.get_tab_mut(new_tab_id) {
1278                            // Associate this tab with the tmux pane
1279                            tab.tmux_pane_id = Some(pane_id);
1280                            tab.set_title(&format!("tmux %{}", pane_id));
1281
1282                            // Start refresh task
1283                            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                            // Route the data to the new tab's terminal
1292                            if let Ok(term) = tab.terminal.try_lock() {
1293                                term.process_data(data);
1294                            }
1295                        }
1296
1297                        // Switch to the new tab (away from gateway tab)
1298                        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    /// Handle pane focus changed notification from external tmux
1320    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        // Update the tmux session's focused pane
1324        if let Some(session) = &mut self.tmux_session {
1325            session.set_focused_pane(Some(tmux_pane_id));
1326        }
1327
1328        // Update the native pane focus to match
1329        if let Some(native_pane_id) = self.tmux_pane_to_native_pane.get(&tmux_pane_id) {
1330            // Find the tab containing this pane and update its focus
1331            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    /// Handle session ended notification
1346    fn handle_tmux_session_ended(&mut self) {
1347        crate::debug_info!("TMUX", "Session ended");
1348
1349        // Collect tmux display tabs to close (tabs with tmux_pane_id set, excluding gateway)
1350        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                // Close tabs that were displaying tmux content (have tmux_pane_id)
1357                // but not the gateway tab itself
1358                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        // Close tmux display tabs
1367        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        // Disable tmux control mode on the gateway tab and clear auto-applied profile
1373        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(); // Clear tmux session profile
1380            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        // Clean up tmux session state
1387        if let Some(mut session) = self.tmux_session.take() {
1388            session.disconnect();
1389        }
1390        self.tmux_session_name = None;
1391
1392        // Clear pane mappings
1393        self.tmux_pane_to_native_pane.clear();
1394        self.native_pane_to_tmux_pane.clear();
1395
1396        // Reset window title (now without tmux info)
1397        self.update_window_title_with_tmux();
1398
1399        // Clear sync state
1400        self.tmux_sync = crate::tmux::TmuxSync::new();
1401
1402        // Show toast
1403        self.show_toast("tmux: Session ended");
1404    }
1405
1406    /// Handle error notification
1407    fn handle_tmux_error(&mut self, msg: &str) {
1408        crate::debug_error!("TMUX", "Error from tmux: {}", msg);
1409
1410        // Show notification to user
1411        self.deliver_notification("tmux Error", msg);
1412    }
1413
1414    /// Handle pause notification (for slow connections)
1415    fn handle_tmux_pause(&mut self) {
1416        crate::debug_info!("TMUX", "Received pause notification - buffering output");
1417
1418        // Set paused state in sync manager
1419        self.tmux_sync.pause();
1420
1421        // Show toast notification to user
1422        self.show_toast("tmux: Output paused (slow connection)");
1423    }
1424
1425    /// Handle continue notification (resume after pause)
1426    fn handle_tmux_continue(&mut self) {
1427        crate::debug_info!("TMUX", "Received continue notification - resuming output");
1428
1429        // Get and flush buffered output
1430        let buffered = self.tmux_sync.resume();
1431
1432        // Flush buffered data to each pane
1433        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                // Find the native pane and send the buffered data
1443                if let Some(native_pane_id) = self.tmux_sync.get_native_pane(tmux_pane_id) {
1444                    // Find the pane across all tabs
1445                    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        // Show toast notification to user
1460        self.show_toast("tmux: Output resumed");
1461    }
1462
1463    /// Process sync actions generated by TmuxSync
1464    #[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    // =========================================================================
1505    // Gateway Mode Session Management
1506    // =========================================================================
1507
1508    /// Initiate a new tmux session via gateway mode.
1509    ///
1510    /// This writes `tmux -CC new-session` to the active tab's PTY and enables
1511    /// tmux control mode parsing. The session will be fully connected once we
1512    /// receive the `%session-changed` notification.
1513    ///
1514    /// # Arguments
1515    /// * `session_name` - Optional session name. If None, tmux will auto-generate one.
1516    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        // Generate the command
1532        let cmd = match session_name {
1533            Some(name) => TmuxSession::create_or_attach_command(name),
1534            None => TmuxSession::create_new_command(None),
1535        };
1536
1537        // Get the active tab ID and write the command to its PTY
1538        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        // Write the command to the PTY
1549        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            // Enable tmux control mode parsing AFTER writing the command
1558            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        // Mark this tab as the gateway
1569        tab.tmux_gateway_active = true;
1570
1571        // Store the gateway tab ID so we know where to send commands
1572        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        // Create session and set gateway state
1580        let mut session = TmuxSession::new();
1581        session.set_gateway_initiating();
1582        self.tmux_session = Some(session);
1583
1584        // Show toast
1585        self.show_toast("tmux: Connecting...");
1586
1587        Ok(())
1588    }
1589
1590    /// Attach to an existing tmux session via gateway mode.
1591    ///
1592    /// This writes `tmux -CC attach -t session` to the active tab's PTY.
1593    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        // Generate the attach command
1605        let cmd = TmuxSession::create_attach_command(session_name);
1606
1607        // Get the active tab ID and write the command to its PTY
1608        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        // Write the command to the PTY
1619        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        // Mark this tab as the gateway
1638        tab.tmux_gateway_active = true;
1639
1640        // Store the gateway tab ID so we know where to send commands
1641        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        // Create session and set gateway state
1649        let mut session = TmuxSession::new();
1650        session.set_gateway_initiating();
1651        self.tmux_session = Some(session);
1652
1653        // Show toast
1654        self.show_toast(format!("tmux: Attaching to '{}'...", session_name));
1655
1656        Ok(())
1657    }
1658
1659    /// Disconnect from the current tmux session
1660    pub fn disconnect_tmux_session(&mut self) {
1661        // Clear the gateway tab ID
1662        self.tmux_gateway_tab_id = None;
1663
1664        // First, disable tmux control mode on any gateway tabs
1665        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        // Clear session name
1680        self.tmux_session_name = None;
1681
1682        // Reset sync state
1683        self.tmux_sync = crate::tmux::TmuxSync::new();
1684
1685        // Reset window title (now without tmux info)
1686        self.update_window_title_with_tmux();
1687    }
1688
1689    /// Check if tmux session is active
1690    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    /// Check if gateway mode is active (connected or connecting)
1697    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    /// Update the tmux focused pane when a native pane is focused
1704    ///
1705    /// This should be called when the user clicks on a pane to ensure
1706    /// input is routed to the correct tmux pane.
1707    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    // =========================================================================
1722    // Gateway Mode Input Routing
1723    // =========================================================================
1724
1725    /// Write a command to the gateway tab's terminal.
1726    ///
1727    /// The gateway tab is where the tmux control mode connection lives.
1728    /// All tmux commands must be written to this tab, not the active tab.
1729    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    /// Send input through tmux gateway mode.
1751    ///
1752    /// When in gateway mode, keyboard input is sent via `send-keys` command
1753    /// written to the gateway tab's PTY. This routes input to the appropriate tmux pane.
1754    ///
1755    /// Returns true if input was handled via tmux, false if it should go to PTY directly.
1756    pub fn send_input_via_tmux(&self, data: &[u8]) -> bool {
1757        // Check if tmux is enabled and connected
1758        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        // Format the send-keys command - try pane-specific first
1774        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                // No focused pane - try window-based routing
1782                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                    // No window mapping either - use untargeted send-keys
1787                    // This sends to tmux's currently active pane
1788                    let escaped = crate::tmux::escape_keys_for_tmux(data);
1789                    format!("send-keys {}\n", escaped)
1790                }
1791            }
1792        };
1793
1794        // Write the command to the gateway tab's PTY
1795        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    /// Format send-keys command for a specific window (if mapping exists)
1804    fn format_send_keys_for_window(&self, data: &[u8]) -> Option<String> {
1805        let active_tab_id = self.tab_manager.active_tab_id()?;
1806
1807        // Find the tmux window for this tab
1808        let tmux_window_id = self.tmux_sync.get_window(active_tab_id)?;
1809
1810        // Format send-keys command with window target using proper escaping
1811        let escaped = crate::tmux::escape_keys_for_tmux(data);
1812        Some(format!("send-keys -t @{} {}\n", tmux_window_id, escaped))
1813    }
1814
1815    /// Send input via tmux window target (fallback when no pane ID is set)
1816    #[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        // Find the tmux window for this tab
1824        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        // Format send-keys command with window target using proper escaping
1837        let escaped = crate::tmux::escape_keys_for_tmux(data);
1838        let cmd = format!("send-keys -t @{} {}\n", tmux_window_id, escaped);
1839
1840        // Write to gateway tab
1841        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    /// Send paste text through tmux gateway mode.
1855    ///
1856    /// Uses send-keys -l for literal text to handle special characters properly.
1857    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        // Format the literal send command
1868        let cmd = match session.format_send_literal(text) {
1869            Some(c) => c,
1870            None => return false,
1871        };
1872
1873        // Write to gateway tab
1874        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    /// Split the current pane via tmux control mode.
1883    ///
1884    /// Writes split-window command to the gateway PTY.
1885    ///
1886    /// # Arguments
1887    /// * `vertical` - true for vertical split (side by side), false for horizontal (stacked)
1888    ///
1889    /// Returns true if the command was sent successfully.
1890    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        // Get the focused pane ID
1901        let pane_id = session.focused_pane();
1902
1903        // Format the split command
1904        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        // Write to gateway tab
1917        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    /// Close the focused pane via tmux control mode.
1930    ///
1931    /// Writes kill-pane command to the gateway PTY.
1932    ///
1933    /// Returns true if the command was sent successfully.
1934    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        // Get the focused pane ID
1945        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        // Write to gateway tab
1956        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    /// Sync clipboard content to tmux paste buffer.
1965    ///
1966    /// Writes set-buffer command to the gateway PTY.
1967    ///
1968    /// Returns true if the command was sent successfully.
1969    pub fn sync_clipboard_to_tmux(&self, content: &str) -> bool {
1970        // Check if clipboard sync is enabled
1971        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        // Don't sync empty content
1980        if content.is_empty() {
1981            return false;
1982        }
1983
1984        // Format the set-buffer command
1985        let escaped = content.replace('\'', "'\\''");
1986        let cmd = format!("set-buffer '{}'\n", escaped);
1987
1988        // Write to gateway tab
1989        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    // =========================================================================
2002    // Pane Resize Sync
2003    // =========================================================================
2004
2005    /// Sync pane resize to tmux after a divider drag.
2006    ///
2007    /// When the user resizes panes by dragging a divider in par-term, this
2008    /// sends the new pane sizes to tmux so external clients see the same layout.
2009    ///
2010    /// # Arguments
2011    /// * `is_horizontal_divider` - true if dragging a horizontal divider (changes heights),
2012    ///   false if dragging a vertical divider (changes widths)
2013    pub fn sync_pane_resize_to_tmux(&self, is_horizontal_divider: bool) {
2014        // Only sync if tmux gateway is active
2015        if !self.is_gateway_active() {
2016            return;
2017        }
2018
2019        // Get cell dimensions from renderer
2020        let (cell_width, cell_height) = match &self.renderer {
2021            Some(r) => (r.cell_width(), r.cell_height()),
2022            None => return,
2023        };
2024
2025        // Get pane sizes from active tab's pane manager
2026        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                    // Get the tmux pane ID for this native pane
2034                    let tmux_pane_id = self.native_pane_to_tmux_pane.get(&pane.id)?;
2035                    // Calculate size in columns/rows
2036                    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        // Send resize commands for each pane, but only for the dimension that changed
2046        // Horizontal divider: changes height (rows) - use -y
2047        // Vertical divider: changes width (cols) - use -x
2048        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    // =========================================================================
2071    // Prefix Key Handling
2072    // =========================================================================
2073
2074    // =========================================================================
2075    // Profile Auto-Switching
2076    // =========================================================================
2077
2078    /// Apply a profile based on tmux session name
2079    ///
2080    /// This checks for profiles that match the session name pattern and applies
2081    /// them to the gateway tab. Profile matching uses glob patterns (e.g., "work-*",
2082    /// "*-production").
2083    fn apply_tmux_session_profile(&mut self, session_name: &str) {
2084        // First, check if there's a fixed tmux_profile configured
2085        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        // Then, check for pattern-based matching
2107        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    /// Apply a profile to the tmux gateway tab
2127    fn apply_profile_to_gateway_tab(
2128        &mut self,
2129        profile_id: crate::profile::ProfileId,
2130        profile_name: &str,
2131    ) {
2132        // Extract profile settings before borrowing tab_manager
2133        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            // Mark the auto-applied profile
2147            tab.auto_applied_profile_id = Some(profile_id);
2148
2149            if let Some((tab_name, icon, badge_text, command, command_args)) = profile_settings {
2150                // Apply profile icon
2151                tab.profile_icon = icon;
2152
2153                // Save original title before overriding (only if not already saved)
2154                if tab.pre_profile_title.is_none() {
2155                    tab.pre_profile_title = Some(tab.title.clone());
2156                }
2157                // Apply profile tab name (fall back to profile name)
2158                tab.title = tab_name.unwrap_or_else(|| profile_name.to_string());
2159
2160                // Apply badge text override if configured
2161                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                // Execute profile command in the running shell if configured
2172                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            // Show notification about profile switch
2193            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        // Apply profile badge settings (color, font, margins, etc.)
2202        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    /// Handle tmux prefix key mode
2209    ///
2210    /// In control mode, we intercept the prefix key (e.g., Ctrl+B or Ctrl+Space)
2211    /// and wait for the next key to translate into a tmux command.
2212    ///
2213    /// Returns true if the key was handled by the prefix system.
2214    pub fn handle_tmux_prefix_key(&mut self, event: &winit::event::KeyEvent) -> bool {
2215        // Only handle on key press
2216        if event.state != winit::event::ElementState::Pressed {
2217            return false;
2218        }
2219
2220        // Only handle if tmux is connected
2221        if !self.config.tmux_enabled || !self.is_tmux_connected() {
2222            return false;
2223        }
2224
2225        let modifiers = self.input_handler.modifiers.state();
2226
2227        // Check if we're in prefix mode (waiting for command key)
2228        if self.tmux_prefix_state.is_active() {
2229            // Ignore modifier-only key presses (Shift, Ctrl, Alt, Super)
2230            // These are needed to type shifted characters like " and %
2231            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; // Don't consume - let the modifier key through
2249            }
2250
2251            // Exit prefix mode
2252            self.tmux_prefix_state.exit();
2253
2254            // Get focused pane ID for targeted commands
2255            let focused_pane = self.tmux_session.as_ref().and_then(|s| s.focused_pane());
2256
2257            // Translate the command key to a tmux command
2258            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                // Send the command to tmux
2269                if self.write_to_gateway(&cmd) {
2270                    // Show toast for certain commands (check command base, ignoring target)
2271                    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                // Unknown command key - show feedback
2281                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; // Consumed the key even if unknown
2292        }
2293
2294        // Check if this is the prefix key
2295        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}