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                    // Tmux layouts always have multiple panes; hide window padding if configured
644                    let effective_padding = if self.config.hide_window_padding_on_split {
645                        0.0
646                    } else {
647                        padding
648                    };
649                    let content_width =
650                        size.width as f32 - effective_padding * 2.0 - content_inset_right;
651                    let content_height = size.height as f32
652                        - content_offset_y
653                        - effective_padding
654                        - status_bar_height;
655                    let bounds = crate::pane::PaneBounds::new(
656                        effective_padding,
657                        content_offset_y,
658                        content_width,
659                        content_height,
660                    );
661                    pm.set_bounds(bounds);
662                    crate::debug_info!(
663                        "TMUX",
664                        "Set pane manager bounds: {}x{} at ({}, {})",
665                        content_width,
666                        content_height,
667                        effective_padding,
668                        content_offset_y
669                    );
670                }
671
672                // Check if we already have mappings for these exact tmux pane IDs
673                // If so, we should preserve the existing native panes/terminals
674                let existing_tmux_ids: std::collections::HashSet<_> =
675                    self.tmux_pane_to_native_pane.keys().copied().collect();
676                let new_tmux_ids: std::collections::HashSet<_> = pane_ids.iter().copied().collect();
677
678                if existing_tmux_ids == new_tmux_ids && !existing_tmux_ids.is_empty() {
679                    // Same panes - preserve terminals but update layout structure
680                    crate::debug_info!(
681                        "TMUX",
682                        "Layout change with same panes ({:?}) - preserving terminals, updating layout",
683                        pane_ids
684                    );
685
686                    // Update the pane tree structure from the new layout without recreating terminals
687                    if let Some(pm) = tab.pane_manager_mut() {
688                        // Update layout structure (ratios, positions) from tmux layout
689                        pm.update_layout_from_tmux(&parsed_layout, &self.tmux_pane_to_native_pane);
690                        pm.recalculate_bounds();
691
692                        // Resize terminals to match new bounds
693                        // No padding in tmux mode - tmux controls the layout
694                        if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info {
695                            pm.resize_all_terminals(cell_width, cell_height);
696                        }
697                    }
698
699                    self.needs_redraw = true;
700                    return; // Early return - don't recreate panes
701                }
702
703                // Check if new panes are a SUBSET of existing (panes were closed)
704                // or if there's overlap (some panes closed, some remain)
705                let panes_to_keep: std::collections::HashSet<_> = existing_tmux_ids
706                    .intersection(&new_tmux_ids)
707                    .copied()
708                    .collect();
709                let panes_to_remove: Vec<_> = existing_tmux_ids
710                    .difference(&new_tmux_ids)
711                    .copied()
712                    .collect();
713                let panes_to_add: Vec<_> = new_tmux_ids
714                    .difference(&existing_tmux_ids)
715                    .copied()
716                    .collect();
717
718                // If we have panes to keep and panes to remove, handle incrementally
719                if !panes_to_keep.is_empty()
720                    && !panes_to_remove.is_empty()
721                    && panes_to_add.is_empty()
722                {
723                    crate::debug_info!(
724                        "TMUX",
725                        "Layout change: keeping {:?}, removing {:?}",
726                        panes_to_keep,
727                        panes_to_remove
728                    );
729
730                    // Check if any of the removed panes was the focused pane
731                    let current_focused = self.tmux_session.as_ref().and_then(|s| s.focused_pane());
732                    let focused_pane_removed = current_focused
733                        .map(|fp| panes_to_remove.contains(&fp))
734                        .unwrap_or(false);
735
736                    // Remove the closed panes from our native pane tree
737                    if let Some(pm) = tab.pane_manager_mut() {
738                        for tmux_pane_id in &panes_to_remove {
739                            if let Some(native_pane_id) =
740                                self.tmux_pane_to_native_pane.get(tmux_pane_id)
741                            {
742                                crate::debug_info!(
743                                    "TMUX",
744                                    "Removing native pane {} for closed tmux pane %{}",
745                                    native_pane_id,
746                                    tmux_pane_id
747                                );
748                                pm.close_pane(*native_pane_id);
749                            }
750                        }
751
752                        // Update layout structure for remaining panes
753                        // Build new mappings with only the kept panes
754                        let kept_mappings: std::collections::HashMap<_, _> = self
755                            .tmux_pane_to_native_pane
756                            .iter()
757                            .filter(|(tmux_id, _)| panes_to_keep.contains(tmux_id))
758                            .map(|(k, v)| (*k, *v))
759                            .collect();
760
761                        pm.update_layout_from_tmux(&parsed_layout, &kept_mappings);
762                        pm.recalculate_bounds();
763
764                        // Resize terminals to match new bounds
765                        if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info {
766                            pm.resize_all_terminals(cell_width, cell_height);
767                        }
768                    }
769
770                    // Update mappings - remove closed panes
771                    for tmux_pane_id in &panes_to_remove {
772                        if let Some(native_id) = self.tmux_pane_to_native_pane.remove(tmux_pane_id)
773                        {
774                            self.native_pane_to_tmux_pane.remove(&native_id);
775                        }
776                    }
777
778                    // If the focused pane was removed, update tmux session focus to first remaining pane
779                    if focused_pane_removed
780                        && let Some(new_focus) = panes_to_keep.iter().next().copied()
781                    {
782                        crate::debug_info!(
783                            "TMUX",
784                            "Focused pane was removed, updating tmux session focus to %{}",
785                            new_focus
786                        );
787                        if let Some(session) = &mut self.tmux_session {
788                            session.set_focused_pane(Some(new_focus));
789                        }
790                    }
791
792                    crate::debug_info!(
793                        "TMUX",
794                        "After pane removal, mappings: {:?}",
795                        self.tmux_pane_to_native_pane
796                    );
797
798                    self.needs_redraw = true;
799                    self.request_redraw();
800                    return; // Early return - don't recreate remaining panes
801                }
802
803                // Handle case where panes are ADDED (split) while keeping existing ones
804                if !panes_to_keep.is_empty()
805                    && !panes_to_add.is_empty()
806                    && panes_to_remove.is_empty()
807                {
808                    crate::debug_info!(
809                        "TMUX",
810                        "Layout change: keeping {:?}, adding {:?}",
811                        panes_to_keep,
812                        panes_to_add
813                    );
814
815                    // Rebuild the entire tree structure from the tmux layout
816                    // This preserves existing pane terminals while creating correct structure
817                    if let Some(pm) = tab.pane_manager_mut() {
818                        // Create a mapping of tmux pane IDs to keep -> their native IDs
819                        let existing_mappings: std::collections::HashMap<_, _> = panes_to_keep
820                            .iter()
821                            .filter_map(|tmux_id| {
822                                self.tmux_pane_to_native_pane
823                                    .get(tmux_id)
824                                    .map(|native_id| (*tmux_id, *native_id))
825                            })
826                            .collect();
827
828                        match pm.rebuild_from_tmux_layout(
829                            &parsed_layout,
830                            &existing_mappings,
831                            &panes_to_add,
832                            &self.config,
833                            std::sync::Arc::clone(&self.runtime),
834                        ) {
835                            Ok(new_mappings) => {
836                                // Update our mappings with the new ones
837                                self.tmux_pane_to_native_pane = new_mappings.clone();
838                                self.native_pane_to_tmux_pane = new_mappings
839                                    .iter()
840                                    .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
841                                    .collect();
842
843                                crate::debug_info!(
844                                    "TMUX",
845                                    "Rebuilt layout with {} panes: {:?}",
846                                    new_mappings.len(),
847                                    new_mappings
848                                );
849
850                                // Resize terminals to match new bounds
851                                if let Some((_, _, _, _, cell_width, cell_height, _)) = bounds_info
852                                {
853                                    pm.resize_all_terminals(cell_width, cell_height);
854                                }
855                            }
856                            Err(e) => {
857                                crate::debug_error!("TMUX", "Failed to rebuild layout: {}", e);
858                            }
859                        }
860                    }
861
862                    // Request content for the new panes only
863                    self.request_pane_refresh(&panes_to_add);
864
865                    crate::debug_info!(
866                        "TMUX",
867                        "After pane addition, mappings: {:?}",
868                        self.tmux_pane_to_native_pane
869                    );
870
871                    self.needs_redraw = true;
872                    self.request_redraw();
873                    return; // Early return - don't recreate all panes
874                }
875
876                // Full layout recreation needed (complete replacement or complex changes)
877                if let Some(pm) = tab.pane_manager_mut() {
878                    crate::debug_info!(
879                        "TMUX",
880                        "Full layout recreation: existing={:?}, new={:?}",
881                        existing_tmux_ids,
882                        new_tmux_ids
883                    );
884
885                    match pm.set_from_tmux_layout(
886                        &parsed_layout,
887                        &self.config,
888                        std::sync::Arc::clone(&self.runtime),
889                    ) {
890                        Ok(pane_mappings) => {
891                            // Store the pane mappings for output routing
892                            crate::debug_info!(
893                                "TMUX",
894                                "Storing pane mappings: {:?}",
895                                pane_mappings
896                            );
897                            // Store both forward and reverse mappings
898                            self.tmux_pane_to_native_pane = pane_mappings.clone();
899                            self.native_pane_to_tmux_pane = pane_mappings
900                                .iter()
901                                .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
902                                .collect();
903
904                            crate::debug_info!(
905                                "TMUX",
906                                "Applied tmux layout to tab {}: {} pane mappings created",
907                                tab_id,
908                                pane_mappings.len()
909                            );
910
911                            // Set tab's tmux_pane_id to first pane for legacy output routing
912                            if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
913                                tab.tmux_pane_id = Some(pane_ids[0]);
914                            }
915
916                            // Request content refresh for all panes
917                            // tmux doesn't send existing content on attach
918                            self.request_pane_refresh(&pane_ids);
919
920                            self.needs_redraw = true;
921                        }
922                        Err(e) => {
923                            crate::debug_error!(
924                                "TMUX",
925                                "Failed to apply tmux layout to tab {}: {}",
926                                tab_id,
927                                e
928                            );
929                            // Fall back to legacy routing
930                            if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
931                                tab.tmux_pane_id = Some(pane_ids[0]);
932                            }
933                        }
934                    }
935                } else {
936                    // No pane manager - use legacy routing
937                    if !pane_ids.is_empty() && tab.tmux_pane_id.is_none() {
938                        tab.tmux_pane_id = Some(pane_ids[0]);
939                        crate::debug_info!(
940                            "TMUX",
941                            "Set tab {} tmux_pane_id to %{} for output routing (no pane manager)",
942                            tab_id,
943                            pane_ids[0]
944                        );
945                    }
946                }
947            }
948        } else {
949            // No tab mapping found - create a new tab for this tmux window
950            crate::debug_info!(
951                "TMUX",
952                "No tab mapping for window @{}, creating new tab for layout",
953                window_id
954            );
955
956            // Create a new tab for this tmux window
957            if self.config.max_tabs == 0 || self.tab_manager.tab_count() < self.config.max_tabs {
958                let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
959                match self.tab_manager.new_tab(
960                    &self.config,
961                    std::sync::Arc::clone(&self.runtime),
962                    false,
963                    grid_size,
964                ) {
965                    Ok(new_tab_id) => {
966                        crate::debug_info!(
967                            "TMUX",
968                            "Created tab {} for tmux window @{}",
969                            new_tab_id,
970                            window_id
971                        );
972
973                        // Register the window mapping
974                        self.tmux_sync.map_window(window_id, new_tab_id);
975
976                        // Now apply the layout to this tab
977                        if let Some(tab) = self.tab_manager.get_tab_mut(new_tab_id) {
978                            tab.init_pane_manager();
979                            tab.set_title(&format!("tmux @{}", window_id));
980
981                            // Start refresh task
982                            if let Some(window) = &self.window {
983                                tab.start_refresh_task(
984                                    std::sync::Arc::clone(&self.runtime),
985                                    std::sync::Arc::clone(window),
986                                    self.config.max_fps,
987                                );
988                            }
989
990                            // Set pane bounds
991                            if let Some((
992                                size,
993                                padding,
994                                content_offset_y,
995                                content_inset_right,
996                                _cell_width,
997                                _cell_height,
998                                status_bar_height,
999                            )) = bounds_info
1000                                && let Some(pm) = tab.pane_manager_mut()
1001                            {
1002                                let content_width =
1003                                    size.width as f32 - padding * 2.0 - content_inset_right;
1004                                let content_height = size.height as f32
1005                                    - content_offset_y
1006                                    - padding
1007                                    - status_bar_height;
1008                                let bounds = crate::pane::PaneBounds::new(
1009                                    padding,
1010                                    content_offset_y,
1011                                    content_width,
1012                                    content_height,
1013                                );
1014                                pm.set_bounds(bounds);
1015                            }
1016
1017                            // Apply the tmux layout
1018                            if let Some(pm) = tab.pane_manager_mut() {
1019                                match pm.set_from_tmux_layout(
1020                                    &parsed_layout,
1021                                    &self.config,
1022                                    std::sync::Arc::clone(&self.runtime),
1023                                ) {
1024                                    Ok(pane_mappings) => {
1025                                        crate::debug_info!(
1026                                            "TMUX",
1027                                            "Storing pane mappings for new tab: {:?}",
1028                                            pane_mappings
1029                                        );
1030                                        // Store both forward and reverse mappings
1031                                        self.native_pane_to_tmux_pane = pane_mappings
1032                                            .iter()
1033                                            .map(|(tmux_id, native_id)| (*native_id, *tmux_id))
1034                                            .collect();
1035                                        self.tmux_pane_to_native_pane = pane_mappings;
1036
1037                                        // Set tab's tmux_pane_id to first pane
1038                                        if !pane_ids.is_empty() {
1039                                            tab.tmux_pane_id = Some(pane_ids[0]);
1040                                        }
1041
1042                                        // Request content refresh for all panes
1043                                        self.request_pane_refresh(&pane_ids);
1044
1045                                        self.needs_redraw = true;
1046                                    }
1047                                    Err(e) => {
1048                                        crate::debug_error!(
1049                                            "TMUX",
1050                                            "Failed to apply tmux layout to new tab: {}",
1051                                            e
1052                                        );
1053                                    }
1054                                }
1055                            }
1056                        }
1057
1058                        // Switch to the new tab
1059                        self.tab_manager.switch_to(new_tab_id);
1060                    }
1061                    Err(e) => {
1062                        crate::debug_error!(
1063                            "TMUX",
1064                            "Failed to create tab for tmux window @{}: {}",
1065                            window_id,
1066                            e
1067                        );
1068                    }
1069                }
1070            }
1071        }
1072    }
1073
1074    /// Log a layout node and its children recursively for debugging
1075    fn log_layout_node(node: &crate::tmux::LayoutNode, depth: usize) {
1076        let indent = "  ".repeat(depth);
1077        match node {
1078            crate::tmux::LayoutNode::Pane {
1079                id,
1080                width,
1081                height,
1082                x,
1083                y,
1084            } => {
1085                crate::debug_trace!(
1086                    "TMUX",
1087                    "{}Pane %{}: {}x{} at ({}, {})",
1088                    indent,
1089                    id,
1090                    width,
1091                    height,
1092                    x,
1093                    y
1094                );
1095            }
1096            crate::tmux::LayoutNode::VerticalSplit {
1097                width,
1098                height,
1099                x,
1100                y,
1101                children,
1102            } => {
1103                crate::debug_trace!(
1104                    "TMUX",
1105                    "{}VerticalSplit: {}x{} at ({}, {}) with {} children",
1106                    indent,
1107                    width,
1108                    height,
1109                    x,
1110                    y,
1111                    children.len()
1112                );
1113                for child in children {
1114                    Self::log_layout_node(child, depth + 1);
1115                }
1116            }
1117            crate::tmux::LayoutNode::HorizontalSplit {
1118                width,
1119                height,
1120                x,
1121                y,
1122                children,
1123            } => {
1124                crate::debug_trace!(
1125                    "TMUX",
1126                    "{}HorizontalSplit: {}x{} at ({}, {}) with {} children",
1127                    indent,
1128                    width,
1129                    height,
1130                    x,
1131                    y,
1132                    children.len()
1133                );
1134                for child in children {
1135                    Self::log_layout_node(child, depth + 1);
1136                }
1137            }
1138        }
1139    }
1140
1141    /// Handle pane output notification - routes to correct terminal
1142    fn handle_tmux_output(&mut self, pane_id: crate::tmux::TmuxPaneId, data: &[u8]) {
1143        if data.is_empty() {
1144            return;
1145        }
1146
1147        crate::debug_trace!(
1148            "TMUX",
1149            "Output from pane %{}: {} bytes",
1150            pane_id,
1151            data.len()
1152        );
1153
1154        // Log first few bytes for debugging space issue
1155        if data.len() <= 20 {
1156            crate::debug_trace!(
1157                "TMUX",
1158                "Output data: {:?} (hex: {:02x?})",
1159                String::from_utf8_lossy(data),
1160                data
1161            );
1162        }
1163
1164        // Check if output is paused - buffer if so
1165        if self.tmux_sync.buffer_output(pane_id, data) {
1166            crate::debug_trace!(
1167                "TMUX",
1168                "Buffered {} bytes for pane %{} (paused)",
1169                data.len(),
1170                pane_id
1171            );
1172            return;
1173        }
1174
1175        // Debug: log the current mapping state
1176        crate::debug_trace!("TMUX", "Pane mappings: {:?}", self.tmux_pane_to_native_pane);
1177
1178        // First, try to find a native pane mapping (for split panes)
1179        // Check our direct mapping first, then fall back to tmux_sync
1180        let native_pane_id = self
1181            .tmux_pane_to_native_pane
1182            .get(&pane_id)
1183            .copied()
1184            .or_else(|| self.tmux_sync.get_native_pane(pane_id));
1185
1186        if let Some(native_pane_id) = native_pane_id {
1187            // Find the pane across all tabs and route output to it
1188            for tab in self.tab_manager.tabs_mut() {
1189                if let Some(pane_manager) = tab.pane_manager_mut()
1190                    && let Some(pane) = pane_manager.get_pane_mut(native_pane_id)
1191                    && let Ok(term) = pane.terminal.try_lock()
1192                {
1193                    // Route the data to this pane's terminal
1194                    term.process_data(data);
1195                    crate::debug_trace!(
1196                        "TMUX",
1197                        "Routed {} bytes to pane {} (tmux %{})",
1198                        data.len(),
1199                        native_pane_id,
1200                        pane_id
1201                    );
1202                    return;
1203                }
1204            }
1205        }
1206
1207        // No native pane mapping - check for tab-level tmux pane mapping
1208        // (This is used when we create tabs for tmux panes without split pane manager)
1209        for tab in self.tab_manager.tabs_mut() {
1210            if tab.tmux_pane_id == Some(pane_id)
1211                && let Ok(term) = tab.terminal.try_lock()
1212            {
1213                term.process_data(data);
1214                crate::debug_trace!(
1215                    "TMUX",
1216                    "Routed {} bytes to tab terminal (tmux %{})",
1217                    data.len(),
1218                    pane_id
1219                );
1220                return;
1221            }
1222        }
1223
1224        // No direct mapping for this pane - try to find an existing tmux tab to route to
1225        // This handles the case where tmux has multiple panes but we don't have native
1226        // split pane rendering yet. Route all output to the first tmux-connected tab.
1227        crate::debug_trace!(
1228            "TMUX",
1229            "No direct mapping for tmux pane %{}, looking for existing tmux tab",
1230            pane_id
1231        );
1232
1233        // First, try to find any tab with a tmux_pane_id set (existing tmux display)
1234        for tab in self.tab_manager.tabs_mut() {
1235            if tab.tmux_pane_id.is_some()
1236                && !tab.tmux_gateway_active
1237                && let Ok(term) = tab.terminal.try_lock()
1238            {
1239                term.process_data(data);
1240                crate::debug_trace!(
1241                    "TMUX",
1242                    "Routed {} bytes from pane %{} to existing tmux tab (pane %{:?})",
1243                    data.len(),
1244                    pane_id,
1245                    tab.tmux_pane_id
1246                );
1247                return;
1248            }
1249        }
1250
1251        // No existing tmux tab found - create one
1252        crate::debug_info!(
1253            "TMUX",
1254            "No existing tmux tab found, creating new tab for pane %{}",
1255            pane_id
1256        );
1257
1258        // Don't route to the gateway tab - that shows raw protocol
1259        // Instead, create a new tab for this tmux pane
1260        if self.tmux_gateway_tab_id.is_some() {
1261            // Check if we can create a new tab
1262            if self.config.max_tabs == 0 || self.tab_manager.tab_count() < self.config.max_tabs {
1263                let grid_size = self.renderer.as_ref().map(|r| r.grid_size());
1264                match self.tab_manager.new_tab(
1265                    &self.config,
1266                    std::sync::Arc::clone(&self.runtime),
1267                    false,
1268                    grid_size,
1269                ) {
1270                    Ok(new_tab_id) => {
1271                        crate::debug_info!(
1272                            "TMUX",
1273                            "Created tab {} for tmux pane %{}",
1274                            new_tab_id,
1275                            pane_id
1276                        );
1277
1278                        // Set the focused pane if not already set
1279                        if let Some(session) = &mut self.tmux_session
1280                            && session.focused_pane().is_none()
1281                        {
1282                            session.set_focused_pane(Some(pane_id));
1283                        }
1284
1285                        // Configure the new tab for this tmux pane
1286                        if let Some(tab) = self.tab_manager.get_tab_mut(new_tab_id) {
1287                            // Associate this tab with the tmux pane
1288                            tab.tmux_pane_id = Some(pane_id);
1289                            tab.set_title(&format!("tmux %{}", pane_id));
1290
1291                            // Start refresh task
1292                            if let Some(window) = &self.window {
1293                                tab.start_refresh_task(
1294                                    std::sync::Arc::clone(&self.runtime),
1295                                    std::sync::Arc::clone(window),
1296                                    self.config.max_fps,
1297                                );
1298                            }
1299
1300                            // Route the data to the new tab's terminal
1301                            if let Ok(term) = tab.terminal.try_lock() {
1302                                term.process_data(data);
1303                            }
1304                        }
1305
1306                        // Switch to the new tab (away from gateway tab)
1307                        self.tab_manager.switch_to(new_tab_id);
1308                    }
1309                    Err(e) => {
1310                        crate::debug_error!(
1311                            "TMUX",
1312                            "Failed to create tab for tmux pane %{}: {}",
1313                            pane_id,
1314                            e
1315                        );
1316                    }
1317                }
1318            } else {
1319                crate::debug_error!(
1320                    "TMUX",
1321                    "Cannot create tab for tmux pane %{}: max tabs reached",
1322                    pane_id
1323                );
1324            }
1325        }
1326    }
1327
1328    /// Handle pane focus changed notification from external tmux
1329    fn handle_tmux_pane_focus_changed(&mut self, tmux_pane_id: crate::tmux::TmuxPaneId) {
1330        crate::debug_info!("TMUX", "Pane focus changed to %{}", tmux_pane_id);
1331
1332        // Update the tmux session's focused pane
1333        if let Some(session) = &mut self.tmux_session {
1334            session.set_focused_pane(Some(tmux_pane_id));
1335        }
1336
1337        // Update the native pane focus to match
1338        if let Some(native_pane_id) = self.tmux_pane_to_native_pane.get(&tmux_pane_id) {
1339            // Find the tab containing this pane and update its focus
1340            if let Some(tab) = self.tab_manager.active_tab_mut()
1341                && let Some(pm) = tab.pane_manager_mut()
1342            {
1343                pm.focus_pane(*native_pane_id);
1344                crate::debug_info!(
1345                    "TMUX",
1346                    "Updated native pane focus: tmux %{} -> native {}",
1347                    tmux_pane_id,
1348                    native_pane_id
1349                );
1350            }
1351        }
1352    }
1353
1354    /// Handle session ended notification
1355    fn handle_tmux_session_ended(&mut self) {
1356        crate::debug_info!("TMUX", "Session ended");
1357
1358        // Collect tmux display tabs to close (tabs with tmux_pane_id set, excluding gateway)
1359        let gateway_tab_id = self.tmux_gateway_tab_id;
1360        let tmux_tabs_to_close: Vec<crate::tab::TabId> = self
1361            .tab_manager
1362            .tabs()
1363            .iter()
1364            .filter_map(|tab| {
1365                // Close tabs that were displaying tmux content (have tmux_pane_id)
1366                // but not the gateway tab itself
1367                if tab.tmux_pane_id.is_some() && Some(tab.id) != gateway_tab_id {
1368                    Some(tab.id)
1369                } else {
1370                    None
1371                }
1372            })
1373            .collect();
1374
1375        // Close tmux display tabs
1376        for tab_id in tmux_tabs_to_close {
1377            crate::debug_info!("TMUX", "Closing tmux display tab {}", tab_id);
1378            let _ = self.tab_manager.close_tab(tab_id);
1379        }
1380
1381        // Disable tmux control mode on the gateway tab and clear auto-applied profile
1382        if let Some(gateway_tab_id) = self.tmux_gateway_tab_id
1383            && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
1384            && tab.tmux_gateway_active
1385        {
1386            tab.tmux_gateway_active = false;
1387            tab.tmux_pane_id = None;
1388            tab.clear_auto_profile(); // Clear tmux session profile
1389            if let Ok(term) = tab.terminal.try_lock() {
1390                term.set_tmux_control_mode(false);
1391            }
1392        }
1393        self.tmux_gateway_tab_id = None;
1394
1395        // Clean up tmux session state
1396        if let Some(mut session) = self.tmux_session.take() {
1397            session.disconnect();
1398        }
1399        self.tmux_session_name = None;
1400
1401        // Clear pane mappings
1402        self.tmux_pane_to_native_pane.clear();
1403        self.native_pane_to_tmux_pane.clear();
1404
1405        // Reset window title (now without tmux info)
1406        self.update_window_title_with_tmux();
1407
1408        // Clear sync state
1409        self.tmux_sync = crate::tmux::TmuxSync::new();
1410
1411        // Show toast
1412        self.show_toast("tmux: Session ended");
1413    }
1414
1415    /// Handle error notification
1416    fn handle_tmux_error(&mut self, msg: &str) {
1417        crate::debug_error!("TMUX", "Error from tmux: {}", msg);
1418
1419        // Show notification to user
1420        self.deliver_notification("tmux Error", msg);
1421    }
1422
1423    /// Handle pause notification (for slow connections)
1424    fn handle_tmux_pause(&mut self) {
1425        crate::debug_info!("TMUX", "Received pause notification - buffering output");
1426
1427        // Set paused state in sync manager
1428        self.tmux_sync.pause();
1429
1430        // Show toast notification to user
1431        self.show_toast("tmux: Output paused (slow connection)");
1432    }
1433
1434    /// Handle continue notification (resume after pause)
1435    fn handle_tmux_continue(&mut self) {
1436        crate::debug_info!("TMUX", "Received continue notification - resuming output");
1437
1438        // Get and flush buffered output
1439        let buffered = self.tmux_sync.resume();
1440
1441        // Flush buffered data to each pane
1442        for (tmux_pane_id, data) in buffered {
1443            if !data.is_empty() {
1444                crate::debug_info!(
1445                    "TMUX",
1446                    "Flushing {} buffered bytes to pane %{}",
1447                    data.len(),
1448                    tmux_pane_id
1449                );
1450
1451                // Find the native pane and send the buffered data
1452                if let Some(native_pane_id) = self.tmux_sync.get_native_pane(tmux_pane_id) {
1453                    // Find the pane across all tabs
1454                    for tab in self.tab_manager.tabs_mut() {
1455                        if let Some(pane_manager) = tab.pane_manager_mut()
1456                            && let Some(pane) = pane_manager.get_pane_mut(native_pane_id)
1457                        {
1458                            if let Ok(term) = pane.terminal.try_lock() {
1459                                term.process_data(&data);
1460                            }
1461                            break;
1462                        }
1463                    }
1464                }
1465            }
1466        }
1467
1468        // Show toast notification to user
1469        self.show_toast("tmux: Output resumed");
1470    }
1471
1472    /// Process sync actions generated by TmuxSync
1473    #[allow(dead_code)]
1474    fn process_sync_actions(&mut self, actions: Vec<SyncAction>) {
1475        for action in actions {
1476            match action {
1477                SyncAction::CreateTab { window_id } => {
1478                    crate::debug_info!("TMUX", "Sync: Create tab for window @{}", window_id);
1479                }
1480                SyncAction::CloseTab { tab_id } => {
1481                    crate::debug_info!("TMUX", "Sync: Close tab {}", tab_id);
1482                }
1483                SyncAction::RenameTab { tab_id, name } => {
1484                    crate::debug_info!("TMUX", "Sync: Rename tab {} to '{}'", tab_id, name);
1485                }
1486                SyncAction::UpdateLayout { tab_id, layout: _ } => {
1487                    crate::debug_info!("TMUX", "Sync: Update layout for tab {}", tab_id);
1488                }
1489                SyncAction::PaneOutput { pane_id, data } => {
1490                    crate::debug_trace!(
1491                        "TMUX",
1492                        "Sync: Route {} bytes to pane {}",
1493                        data.len(),
1494                        pane_id
1495                    );
1496                }
1497                SyncAction::SessionEnded => {
1498                    crate::debug_info!("TMUX", "Sync: Session ended");
1499                    self.handle_tmux_session_ended();
1500                }
1501                SyncAction::Pause => {
1502                    crate::debug_info!("TMUX", "Sync: Pause");
1503                    self.handle_tmux_pause();
1504                }
1505                SyncAction::Continue => {
1506                    crate::debug_info!("TMUX", "Sync: Continue");
1507                    self.handle_tmux_continue();
1508                }
1509            }
1510        }
1511    }
1512
1513    // =========================================================================
1514    // Gateway Mode Session Management
1515    // =========================================================================
1516
1517    /// Initiate a new tmux session via gateway mode.
1518    ///
1519    /// This writes `tmux -CC new-session` to the active tab's PTY and enables
1520    /// tmux control mode parsing. The session will be fully connected once we
1521    /// receive the `%session-changed` notification.
1522    ///
1523    /// # Arguments
1524    /// * `session_name` - Optional session name. If None, tmux will auto-generate one.
1525    pub fn initiate_tmux_gateway(&mut self, session_name: Option<&str>) -> anyhow::Result<()> {
1526        if !self.config.tmux_enabled {
1527            anyhow::bail!("tmux integration is disabled");
1528        }
1529
1530        if self.tmux_session.is_some() && self.is_tmux_connected() {
1531            anyhow::bail!("Already connected to a tmux session");
1532        }
1533
1534        crate::debug_info!(
1535            "TMUX",
1536            "Initiating gateway mode session: {:?}",
1537            session_name.unwrap_or("(auto)")
1538        );
1539
1540        // Generate the command
1541        let cmd = match session_name {
1542            Some(name) => TmuxSession::create_or_attach_command(name),
1543            None => TmuxSession::create_new_command(None),
1544        };
1545
1546        // Get the active tab ID and write the command to its PTY
1547        let gateway_tab_id = self
1548            .tab_manager
1549            .active_tab_id()
1550            .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1551
1552        let tab = self
1553            .tab_manager
1554            .active_tab_mut()
1555            .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1556
1557        // Write the command to the PTY
1558        if let Ok(term) = tab.terminal.try_lock() {
1559            crate::debug_info!(
1560                "TMUX",
1561                "Writing gateway command to tab {}: {}",
1562                gateway_tab_id,
1563                cmd.trim()
1564            );
1565            term.write(cmd.as_bytes())?;
1566            // Enable tmux control mode parsing AFTER writing the command
1567            term.set_tmux_control_mode(true);
1568            crate::debug_info!(
1569                "TMUX",
1570                "Enabled tmux control mode parsing on tab {}",
1571                gateway_tab_id
1572            );
1573        } else {
1574            anyhow::bail!("Could not acquire terminal lock");
1575        }
1576
1577        // Mark this tab as the gateway
1578        tab.tmux_gateway_active = true;
1579
1580        // Store the gateway tab ID so we know where to send commands
1581        self.tmux_gateway_tab_id = Some(gateway_tab_id);
1582        crate::debug_info!(
1583            "TMUX",
1584            "Gateway tab set to {}, state: Initiating",
1585            gateway_tab_id
1586        );
1587
1588        // Create session and set gateway state
1589        let mut session = TmuxSession::new();
1590        session.set_gateway_initiating();
1591        self.tmux_session = Some(session);
1592
1593        // Show toast
1594        self.show_toast("tmux: Connecting...");
1595
1596        Ok(())
1597    }
1598
1599    /// Attach to an existing tmux session via gateway mode.
1600    ///
1601    /// This writes `tmux -CC attach -t session` to the active tab's PTY.
1602    pub fn attach_tmux_gateway(&mut self, session_name: &str) -> anyhow::Result<()> {
1603        if !self.config.tmux_enabled {
1604            anyhow::bail!("tmux integration is disabled");
1605        }
1606
1607        if self.tmux_session.is_some() && self.is_tmux_connected() {
1608            anyhow::bail!("Already connected to a tmux session");
1609        }
1610
1611        crate::debug_info!("TMUX", "Attaching to session via gateway: {}", session_name);
1612
1613        // Generate the attach command
1614        let cmd = TmuxSession::create_attach_command(session_name);
1615
1616        // Get the active tab ID and write the command to its PTY
1617        let gateway_tab_id = self
1618            .tab_manager
1619            .active_tab_id()
1620            .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1621
1622        let tab = self
1623            .tab_manager
1624            .active_tab_mut()
1625            .ok_or_else(|| anyhow::anyhow!("No active tab available for tmux gateway"))?;
1626
1627        // Write the command to the PTY
1628        if let Ok(term) = tab.terminal.try_lock() {
1629            crate::debug_info!(
1630                "TMUX",
1631                "Writing attach command to tab {}: {}",
1632                gateway_tab_id,
1633                cmd.trim()
1634            );
1635            term.write(cmd.as_bytes())?;
1636            term.set_tmux_control_mode(true);
1637            crate::debug_info!(
1638                "TMUX",
1639                "Enabled tmux control mode parsing on tab {}",
1640                gateway_tab_id
1641            );
1642        } else {
1643            anyhow::bail!("Could not acquire terminal lock");
1644        }
1645
1646        // Mark this tab as the gateway
1647        tab.tmux_gateway_active = true;
1648
1649        // Store the gateway tab ID so we know where to send commands
1650        self.tmux_gateway_tab_id = Some(gateway_tab_id);
1651        crate::debug_info!(
1652            "TMUX",
1653            "Gateway tab set to {}, state: Initiating",
1654            gateway_tab_id
1655        );
1656
1657        // Create session and set gateway state
1658        let mut session = TmuxSession::new();
1659        session.set_gateway_initiating();
1660        self.tmux_session = Some(session);
1661
1662        // Show toast
1663        self.show_toast(format!("tmux: Attaching to '{}'...", session_name));
1664
1665        Ok(())
1666    }
1667
1668    /// Disconnect from the current tmux session
1669    pub fn disconnect_tmux_session(&mut self) {
1670        // Clear the gateway tab ID
1671        self.tmux_gateway_tab_id = None;
1672
1673        // First, disable tmux control mode on any gateway tabs
1674        for tab in self.tab_manager.tabs_mut() {
1675            if tab.tmux_gateway_active {
1676                tab.tmux_gateway_active = false;
1677                if let Ok(term) = tab.terminal.try_lock() {
1678                    term.set_tmux_control_mode(false);
1679                }
1680            }
1681        }
1682
1683        if let Some(mut session) = self.tmux_session.take() {
1684            crate::debug_info!("TMUX", "Disconnecting from tmux session");
1685            session.disconnect();
1686        }
1687
1688        // Clear session name
1689        self.tmux_session_name = None;
1690
1691        // Reset sync state
1692        self.tmux_sync = crate::tmux::TmuxSync::new();
1693
1694        // Reset window title (now without tmux info)
1695        self.update_window_title_with_tmux();
1696    }
1697
1698    /// Check if tmux session is active
1699    pub fn is_tmux_connected(&self) -> bool {
1700        self.tmux_session
1701            .as_ref()
1702            .is_some_and(|s| s.state() == SessionState::Connected)
1703    }
1704
1705    /// Check if gateway mode is active (connected or connecting)
1706    pub fn is_gateway_active(&self) -> bool {
1707        self.tmux_session
1708            .as_ref()
1709            .is_some_and(|s| s.is_gateway_active())
1710    }
1711
1712    /// Update the tmux focused pane when a native pane is focused
1713    ///
1714    /// This should be called when the user clicks on a pane to ensure
1715    /// input is routed to the correct tmux pane.
1716    pub fn set_tmux_focused_pane_from_native(&mut self, native_pane_id: crate::pane::PaneId) {
1717        if let Some(tmux_pane_id) = self.native_pane_to_tmux_pane.get(&native_pane_id)
1718            && let Some(session) = &mut self.tmux_session
1719        {
1720            crate::debug_info!(
1721                "TMUX",
1722                "Setting focused pane: native {} -> tmux %{}",
1723                native_pane_id,
1724                tmux_pane_id
1725            );
1726            session.set_focused_pane(Some(*tmux_pane_id));
1727        }
1728    }
1729
1730    // =========================================================================
1731    // Gateway Mode Input Routing
1732    // =========================================================================
1733
1734    /// Write a command to the gateway tab's terminal.
1735    ///
1736    /// The gateway tab is where the tmux control mode connection lives.
1737    /// All tmux commands must be written to this tab, not the active tab.
1738    fn write_to_gateway(&self, cmd: &str) -> bool {
1739        let gateway_tab_id = match self.tmux_gateway_tab_id {
1740            Some(id) => id,
1741            None => {
1742                crate::debug_trace!("TMUX", "No gateway tab ID set");
1743                return false;
1744            }
1745        };
1746
1747        if let Some(tab) = self.tab_manager.get_tab(gateway_tab_id)
1748            && tab.tmux_gateway_active
1749            && let Ok(term) = tab.terminal.try_lock()
1750            && term.write(cmd.as_bytes()).is_ok()
1751        {
1752            return true;
1753        }
1754
1755        crate::debug_trace!("TMUX", "Failed to write to gateway tab");
1756        false
1757    }
1758
1759    /// Send input through tmux gateway mode.
1760    ///
1761    /// When in gateway mode, keyboard input is sent via `send-keys` command
1762    /// written to the gateway tab's PTY. This routes input to the appropriate tmux pane.
1763    ///
1764    /// Returns true if input was handled via tmux, false if it should go to PTY directly.
1765    pub fn send_input_via_tmux(&self, data: &[u8]) -> bool {
1766        // Check if tmux is enabled and connected
1767        if !self.config.tmux_enabled || !self.is_tmux_connected() {
1768            crate::debug_trace!(
1769                "TMUX",
1770                "send_input_via_tmux: not sending - enabled={}, connected={}",
1771                self.config.tmux_enabled,
1772                self.is_tmux_connected()
1773            );
1774            return false;
1775        }
1776
1777        let session = match &self.tmux_session {
1778            Some(s) => s,
1779            None => return false,
1780        };
1781
1782        // Format the send-keys command - try pane-specific first
1783        let cmd = match session.format_send_keys(data) {
1784            Some(c) => {
1785                crate::debug_trace!("TMUX", "Using pane-specific send-keys: {}", c.trim());
1786                c
1787            }
1788            None => {
1789                crate::debug_trace!("TMUX", "No focused pane for send-keys, trying window-based");
1790                // No focused pane - try window-based routing
1791                if let Some(cmd) = self.format_send_keys_for_window(data) {
1792                    crate::debug_trace!("TMUX", "Using window-based send-keys: {}", cmd.trim());
1793                    cmd
1794                } else {
1795                    // No window mapping either - use untargeted send-keys
1796                    // This sends to tmux's currently active pane
1797                    let escaped = crate::tmux::escape_keys_for_tmux(data);
1798                    format!("send-keys {}\n", escaped)
1799                }
1800            }
1801        };
1802
1803        // Write the command to the gateway tab's PTY
1804        if self.write_to_gateway(&cmd) {
1805            crate::debug_trace!("TMUX", "Sent {} bytes via gateway send-keys", data.len());
1806            return true;
1807        }
1808
1809        false
1810    }
1811
1812    /// Format send-keys command for a specific window (if mapping exists)
1813    fn format_send_keys_for_window(&self, data: &[u8]) -> Option<String> {
1814        let active_tab_id = self.tab_manager.active_tab_id()?;
1815
1816        // Find the tmux window for this tab
1817        let tmux_window_id = self.tmux_sync.get_window(active_tab_id)?;
1818
1819        // Format send-keys command with window target using proper escaping
1820        let escaped = crate::tmux::escape_keys_for_tmux(data);
1821        Some(format!("send-keys -t @{} {}\n", tmux_window_id, escaped))
1822    }
1823
1824    /// Send input via tmux window target (fallback when no pane ID is set)
1825    #[allow(dead_code)]
1826    fn send_input_via_tmux_window(&self, data: &[u8]) -> bool {
1827        let active_tab_id = match self.tab_manager.active_tab_id() {
1828            Some(id) => id,
1829            None => return false,
1830        };
1831
1832        // Find the tmux window for this tab
1833        let tmux_window_id = match self.tmux_sync.get_window(active_tab_id) {
1834            Some(id) => id,
1835            None => {
1836                crate::debug_trace!(
1837                    "TMUX",
1838                    "No tmux window mapping for tab {}, using untargeted send-keys",
1839                    active_tab_id
1840                );
1841                return false;
1842            }
1843        };
1844
1845        // Format send-keys command with window target using proper escaping
1846        let escaped = crate::tmux::escape_keys_for_tmux(data);
1847        let cmd = format!("send-keys -t @{} {}\n", tmux_window_id, escaped);
1848
1849        // Write to gateway tab
1850        if self.write_to_gateway(&cmd) {
1851            crate::debug_trace!(
1852                "TMUX",
1853                "Sent {} bytes via gateway to window @{}",
1854                data.len(),
1855                tmux_window_id
1856            );
1857            return true;
1858        }
1859
1860        false
1861    }
1862
1863    /// Send paste text through tmux gateway mode.
1864    ///
1865    /// Uses send-keys -l for literal text to handle special characters properly.
1866    pub fn paste_via_tmux(&self, text: &str) -> bool {
1867        if !self.config.tmux_enabled || !self.is_tmux_connected() {
1868            return false;
1869        }
1870
1871        let session = match &self.tmux_session {
1872            Some(s) => s,
1873            None => return false,
1874        };
1875
1876        // Format the literal send command
1877        let cmd = match session.format_send_literal(text) {
1878            Some(c) => c,
1879            None => return false,
1880        };
1881
1882        // Write to gateway tab
1883        if self.write_to_gateway(&cmd) {
1884            crate::debug_info!("TMUX", "Pasted {} chars via gateway", text.len());
1885            return true;
1886        }
1887
1888        false
1889    }
1890
1891    /// Split the current pane via tmux control mode.
1892    ///
1893    /// Writes split-window command to the gateway PTY.
1894    ///
1895    /// # Arguments
1896    /// * `vertical` - true for vertical split (side by side), false for horizontal (stacked)
1897    ///
1898    /// Returns true if the command was sent successfully.
1899    pub fn split_pane_via_tmux(&self, vertical: bool) -> bool {
1900        if !self.config.tmux_enabled || !self.is_tmux_connected() {
1901            return false;
1902        }
1903
1904        let session = match &self.tmux_session {
1905            Some(s) => s,
1906            None => return false,
1907        };
1908
1909        // Get the focused pane ID
1910        let pane_id = session.focused_pane();
1911
1912        // Format the split command
1913        let cmd = if vertical {
1914            match pane_id {
1915                Some(id) => format!("split-window -h -t %{}\n", id),
1916                None => "split-window -h\n".to_string(),
1917            }
1918        } else {
1919            match pane_id {
1920                Some(id) => format!("split-window -v -t %{}\n", id),
1921                None => "split-window -v\n".to_string(),
1922            }
1923        };
1924
1925        // Write to gateway tab
1926        if self.write_to_gateway(&cmd) {
1927            crate::debug_info!(
1928                "TMUX",
1929                "Sent {} split command via gateway",
1930                if vertical { "vertical" } else { "horizontal" }
1931            );
1932            return true;
1933        }
1934
1935        false
1936    }
1937
1938    /// Close the focused pane via tmux control mode.
1939    ///
1940    /// Writes kill-pane command to the gateway PTY.
1941    ///
1942    /// Returns true if the command was sent successfully.
1943    pub fn close_pane_via_tmux(&self) -> bool {
1944        if !self.config.tmux_enabled || !self.is_tmux_connected() {
1945            return false;
1946        }
1947
1948        let session = match &self.tmux_session {
1949            Some(s) => s,
1950            None => return false,
1951        };
1952
1953        // Get the focused pane ID
1954        let pane_id = match session.focused_pane() {
1955            Some(id) => id,
1956            None => {
1957                crate::debug_info!("TMUX", "No focused pane to close");
1958                return false;
1959            }
1960        };
1961
1962        let cmd = format!("kill-pane -t %{}\n", pane_id);
1963
1964        // Write to gateway tab
1965        if self.write_to_gateway(&cmd) {
1966            crate::debug_info!("TMUX", "Sent kill-pane command for pane %{}", pane_id);
1967            return true;
1968        }
1969
1970        false
1971    }
1972
1973    /// Sync clipboard content to tmux paste buffer.
1974    ///
1975    /// Writes set-buffer command to the gateway PTY.
1976    ///
1977    /// Returns true if the command was sent successfully.
1978    pub fn sync_clipboard_to_tmux(&self, content: &str) -> bool {
1979        // Check if clipboard sync is enabled
1980        if !self.config.tmux_clipboard_sync {
1981            return false;
1982        }
1983
1984        if !self.config.tmux_enabled || !self.is_tmux_connected() {
1985            return false;
1986        }
1987
1988        // Don't sync empty content
1989        if content.is_empty() {
1990            return false;
1991        }
1992
1993        // Format the set-buffer command
1994        let escaped = content.replace('\'', "'\\''");
1995        let cmd = format!("set-buffer '{}'\n", escaped);
1996
1997        // Write to gateway tab
1998        if self.write_to_gateway(&cmd) {
1999            crate::debug_trace!(
2000                "TMUX",
2001                "Synced {} chars to tmux paste buffer",
2002                content.len()
2003            );
2004            return true;
2005        }
2006
2007        false
2008    }
2009
2010    // =========================================================================
2011    // Pane Resize Sync
2012    // =========================================================================
2013
2014    /// Sync pane resize to tmux after a divider drag.
2015    ///
2016    /// When the user resizes panes by dragging a divider in par-term, this
2017    /// sends the new pane sizes to tmux so external clients see the same layout.
2018    ///
2019    /// # Arguments
2020    /// * `is_horizontal_divider` - true if dragging a horizontal divider (changes heights),
2021    ///   false if dragging a vertical divider (changes widths)
2022    pub fn sync_pane_resize_to_tmux(&self, is_horizontal_divider: bool) {
2023        // Only sync if tmux gateway is active
2024        if !self.is_gateway_active() {
2025            return;
2026        }
2027
2028        // Get cell dimensions from renderer
2029        let (cell_width, cell_height) = match &self.renderer {
2030            Some(r) => (r.cell_width(), r.cell_height()),
2031            None => return,
2032        };
2033
2034        // Get pane sizes from active tab's pane manager
2035        let pane_sizes: Vec<(crate::tmux::TmuxPaneId, usize, usize)> = if let Some(tab) =
2036            self.tab_manager.active_tab()
2037            && let Some(pm) = tab.pane_manager()
2038        {
2039            pm.all_panes()
2040                .iter()
2041                .filter_map(|pane| {
2042                    // Get the tmux pane ID for this native pane
2043                    let tmux_pane_id = self.native_pane_to_tmux_pane.get(&pane.id)?;
2044                    // Calculate size in columns/rows
2045                    let cols = (pane.bounds.width / cell_width).floor() as usize;
2046                    let rows = (pane.bounds.height / cell_height).floor() as usize;
2047                    Some((*tmux_pane_id, cols.max(1), rows.max(1)))
2048                })
2049                .collect()
2050        } else {
2051            return;
2052        };
2053
2054        // Send resize commands for each pane, but only for the dimension that changed
2055        // Horizontal divider: changes height (rows) - use -y
2056        // Vertical divider: changes width (cols) - use -x
2057        for (tmux_pane_id, cols, rows) in pane_sizes {
2058            let cmd = if is_horizontal_divider {
2059                format!("resize-pane -t %{} -y {}\n", tmux_pane_id, rows)
2060            } else {
2061                format!("resize-pane -t %{} -x {}\n", tmux_pane_id, cols)
2062            };
2063            if self.write_to_gateway(&cmd) {
2064                crate::debug_info!(
2065                    "TMUX",
2066                    "Synced pane %{} {} resize to {}",
2067                    tmux_pane_id,
2068                    if is_horizontal_divider {
2069                        "height"
2070                    } else {
2071                        "width"
2072                    },
2073                    if is_horizontal_divider { rows } else { cols }
2074                );
2075            }
2076        }
2077    }
2078
2079    // =========================================================================
2080    // Prefix Key Handling
2081    // =========================================================================
2082
2083    // =========================================================================
2084    // Profile Auto-Switching
2085    // =========================================================================
2086
2087    /// Apply a profile based on tmux session name
2088    ///
2089    /// This checks for profiles that match the session name pattern and applies
2090    /// them to the gateway tab. Profile matching uses glob patterns (e.g., "work-*",
2091    /// "*-production").
2092    fn apply_tmux_session_profile(&mut self, session_name: &str) {
2093        // First, check if there's a fixed tmux_profile configured
2094        if let Some(ref profile_name) = self.config.tmux_profile {
2095            if let Some(profile) = self.profile_manager.find_by_name(profile_name) {
2096                let profile_id = profile.id;
2097                let profile_display = profile.name.clone();
2098                crate::debug_info!(
2099                    "TMUX",
2100                    "Applying configured tmux_profile '{}' for session '{}'",
2101                    profile_display,
2102                    session_name
2103                );
2104                self.apply_profile_to_gateway_tab(profile_id, &profile_display);
2105                return;
2106            } else {
2107                crate::debug_info!(
2108                    "TMUX",
2109                    "Configured tmux_profile '{}' not found",
2110                    profile_name
2111                );
2112            }
2113        }
2114
2115        // Then, check for pattern-based matching
2116        if let Some(profile) = self.profile_manager.find_by_tmux_session(session_name) {
2117            let profile_id = profile.id;
2118            let profile_display = profile.name.clone();
2119            crate::debug_info!(
2120                "TMUX",
2121                "Auto-switching to profile '{}' for tmux session '{}'",
2122                profile_display,
2123                session_name
2124            );
2125            self.apply_profile_to_gateway_tab(profile_id, &profile_display);
2126        } else {
2127            crate::debug_info!(
2128                "TMUX",
2129                "No profile matches tmux session '{}' - consider adding tmux_session_patterns to a profile",
2130                session_name
2131            );
2132        }
2133    }
2134
2135    /// Apply a profile to the tmux gateway tab
2136    fn apply_profile_to_gateway_tab(
2137        &mut self,
2138        profile_id: crate::profile::ProfileId,
2139        profile_name: &str,
2140    ) {
2141        // Extract profile settings before borrowing tab_manager
2142        let profile_settings = self.profile_manager.get(&profile_id).map(|p| {
2143            (
2144                p.tab_name.clone(),
2145                p.icon.clone(),
2146                p.badge_text.clone(),
2147                p.command.clone(),
2148                p.command_args.clone(),
2149            )
2150        });
2151
2152        if let Some(gateway_tab_id) = self.tmux_gateway_tab_id
2153            && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
2154        {
2155            // Mark the auto-applied profile
2156            tab.auto_applied_profile_id = Some(profile_id);
2157
2158            if let Some((tab_name, icon, badge_text, command, command_args)) = profile_settings {
2159                // Apply profile icon
2160                tab.profile_icon = icon;
2161
2162                // Save original title before overriding (only if not already saved)
2163                if tab.pre_profile_title.is_none() {
2164                    tab.pre_profile_title = Some(tab.title.clone());
2165                }
2166                // Apply profile tab name (fall back to profile name)
2167                tab.title = tab_name.unwrap_or_else(|| profile_name.to_string());
2168
2169                // Apply badge text override if configured
2170                if let Some(badge_text) = badge_text {
2171                    tab.badge_override = Some(badge_text.clone());
2172                    crate::debug_info!(
2173                        "TMUX",
2174                        "Applied badge text '{}' from profile '{}'",
2175                        badge_text,
2176                        profile_name
2177                    );
2178                }
2179
2180                // Execute profile command in the running shell if configured
2181                if let Some(cmd) = command {
2182                    let mut full_cmd = cmd;
2183                    if let Some(args) = command_args {
2184                        for arg in args {
2185                            full_cmd.push(' ');
2186                            full_cmd.push_str(&arg);
2187                        }
2188                    }
2189                    full_cmd.push('\n');
2190
2191                    let terminal_clone = std::sync::Arc::clone(&tab.terminal);
2192                    self.runtime.spawn(async move {
2193                        let term = terminal_clone.lock().await;
2194                        if let Err(e) = term.write(full_cmd.as_bytes()) {
2195                            log::error!("Failed to execute tmux profile command: {}", e);
2196                        }
2197                    });
2198                }
2199            }
2200
2201            // Show notification about profile switch
2202            self.show_toast(format!("tmux: Profile '{}' applied", profile_name));
2203            log::info!(
2204                "Applied profile '{}' for tmux session (gateway tab {})",
2205                profile_name,
2206                gateway_tab_id
2207            );
2208        }
2209
2210        // Apply profile badge settings (color, font, margins, etc.)
2211        if let Some(profile) = self.profile_manager.get(&profile_id) {
2212            let profile_clone = profile.clone();
2213            self.apply_profile_badge(&profile_clone);
2214        }
2215    }
2216
2217    /// Handle tmux prefix key mode
2218    ///
2219    /// In control mode, we intercept the prefix key (e.g., Ctrl+B or Ctrl+Space)
2220    /// and wait for the next key to translate into a tmux command.
2221    ///
2222    /// Returns true if the key was handled by the prefix system.
2223    pub fn handle_tmux_prefix_key(&mut self, event: &winit::event::KeyEvent) -> bool {
2224        // Only handle on key press
2225        if event.state != winit::event::ElementState::Pressed {
2226            return false;
2227        }
2228
2229        // Only handle if tmux is connected
2230        if !self.config.tmux_enabled || !self.is_tmux_connected() {
2231            return false;
2232        }
2233
2234        let modifiers = self.input_handler.modifiers.state();
2235
2236        // Check if we're in prefix mode (waiting for command key)
2237        if self.tmux_prefix_state.is_active() {
2238            // Ignore modifier-only key presses (Shift, Ctrl, Alt, Super)
2239            // These are needed to type shifted characters like " and %
2240            use winit::keyboard::{Key, NamedKey};
2241            let is_modifier_only = matches!(
2242                event.logical_key,
2243                Key::Named(
2244                    NamedKey::Shift
2245                        | NamedKey::Control
2246                        | NamedKey::Alt
2247                        | NamedKey::Super
2248                        | NamedKey::Meta
2249                )
2250            );
2251            if is_modifier_only {
2252                crate::debug_trace!(
2253                    "TMUX",
2254                    "Ignoring modifier-only key in prefix mode: {:?}",
2255                    event.logical_key
2256                );
2257                return false; // Don't consume - let the modifier key through
2258            }
2259
2260            // Exit prefix mode
2261            self.tmux_prefix_state.exit();
2262
2263            // Get focused pane ID for targeted commands
2264            let focused_pane = self.tmux_session.as_ref().and_then(|s| s.focused_pane());
2265
2266            // Translate the command key to a tmux command
2267            if let Some(cmd) =
2268                crate::tmux::translate_command_key(&event.logical_key, modifiers, focused_pane)
2269            {
2270                crate::debug_info!(
2271                    "TMUX",
2272                    "Prefix command: {:?} -> {}",
2273                    event.logical_key,
2274                    cmd.trim()
2275                );
2276
2277                // Send the command to tmux
2278                if self.write_to_gateway(&cmd) {
2279                    // Show toast for certain commands (check command base, ignoring target)
2280                    let cmd_base = cmd.split(" -t").next().unwrap_or(&cmd).trim();
2281                    match cmd_base {
2282                        "detach-client" => self.show_toast("tmux: Detaching..."),
2283                        "new-window" => self.show_toast("tmux: New window"),
2284                        _ => {}
2285                    }
2286                    return true;
2287                }
2288            } else {
2289                // Unknown command key - show feedback
2290                crate::debug_info!(
2291                    "TMUX",
2292                    "Unknown prefix command key: {:?}",
2293                    event.logical_key
2294                );
2295                self.show_toast(format!(
2296                    "tmux: Unknown command key: {:?}",
2297                    event.logical_key
2298                ));
2299            }
2300            return true; // Consumed the key even if unknown
2301        }
2302
2303        // Check if this is the prefix key
2304        if let Some(ref prefix_key) = self.tmux_prefix_key
2305            && prefix_key.matches(&event.logical_key, modifiers)
2306        {
2307            crate::debug_info!("TMUX", "Prefix key pressed, entering prefix mode");
2308            self.tmux_prefix_state.enter();
2309            self.show_toast("tmux: prefix...");
2310            return true;
2311        }
2312
2313        false
2314    }
2315}