par-term 0.48.0

Cross-platform GPU-accelerated terminal emulator with inline graphics support (Sixel, iTerm2, Kitty)
//! Flow control and miscellaneous tmux notification handlers.
//!
//! Covers pane focus changes, error reporting, pause/continue flow control,
//! and the (currently dead-code) sync-action dispatcher.

use crate::app::window_state::WindowState;
use crate::tmux::SyncAction;

impl WindowState {
    /// Handle pane focus changed notification from external tmux
    pub(super) fn handle_tmux_pane_focus_changed(&mut self, tmux_pane_id: crate::tmux::TmuxPaneId) {
        crate::debug_info!("TMUX", "Pane focus changed to %{}", tmux_pane_id);

        // Update the tmux session's focused pane
        if let Some(session) = &mut self.tmux_state.tmux_session {
            session.set_focused_pane(Some(tmux_pane_id));
        }

        // Update the native pane focus to match — inside the pane's owning
        // tab (native pane ids restart at 1 in every tab; focusing by id in
        // whichever tab happens to be active could focus another window's
        // pane).
        if let Some((owner_tab_id, native_pane_id)) = self.tmux_state.tmux_pane_owner(tmux_pane_id)
            && let Some(tab) = self.tab_manager.get_tab_mut(owner_tab_id)
            && let Some(pm) = tab.pane_manager_mut()
        {
            pm.focus_pane(native_pane_id);
            crate::debug_info!(
                "TMUX",
                "Updated native pane focus: tmux %{} -> native {}",
                tmux_pane_id,
                native_pane_id
            );
        }
    }

    /// Handle error notification
    pub(super) fn handle_tmux_error(&mut self, msg: &str) {
        crate::debug_error!("TMUX", "Error from tmux: {}", msg);

        // UX.md T8: the mux drain and the tmux gateway both dispatch here;
        // the notification title must not say "tmux" on a par-mux path.
        let title = if self.tmux_state.is_mux_attached() {
            "par-mux error"
        } else {
            "tmux Error"
        };
        self.deliver_notification(title, msg);
    }

    /// Handle pause notification (for slow connections)
    pub(super) fn handle_tmux_pause(&mut self) {
        crate::debug_info!("TMUX", "Received pause notification - buffering output");

        // Set paused state in sync manager
        self.tmux_state.tmux_sync.pause();

        // Show toast notification to user
        self.show_toast("Output paused (slow connection)");
    }

    /// Handle continue notification (resume after pause)
    pub(super) fn handle_tmux_continue(&mut self) {
        crate::debug_info!("TMUX", "Received continue notification - resuming output");

        // Get and flush buffered output
        let buffered = self.tmux_state.tmux_sync.resume();

        // Flush buffered data to each pane
        for (tmux_pane_id, data) in buffered {
            if !data.is_empty() {
                crate::debug_info!(
                    "TMUX",
                    "Flushing {} buffered bytes to pane %{}",
                    data.len(),
                    tmux_pane_id
                );

                // Find the native pane and send the buffered data
                if let Some(native_pane_id) =
                    self.tmux_state.tmux_sync.get_native_pane(tmux_pane_id)
                {
                    // Find the pane across all tabs
                    for tab in self.tab_manager.tabs_mut() {
                        if let Some(pane_manager) = tab.pane_manager_mut()
                            && let Some(pane) = pane_manager.get_pane_mut(native_pane_id)
                        {
                            // try_lock: intentional — flushing buffered output in the sync
                            // event loop. On miss: this buffered chunk is lost. Low risk:
                            // the tmux %continue state means output has resumed and fresh
                            // data will arrive shortly to fill in any gap.
                            if let Ok(term) = pane.terminal.try_read() {
                                term.process_mux_output(&data);
                            }
                            break;
                        }
                    }
                }
            }
        }

        // Show toast notification to user
        self.show_toast("Output resumed");
    }

    /// Process sync actions generated by `TmuxSync::process_notifications`.
    ///
    /// Called from `check_tmux_notifications` (polling.rs) once per group of notifications
    /// (session → layout → output → other) to drive UI state mutations with pre-translated
    /// native IDs.
    ///
    /// Returns `true` if any action requires a redraw.
    pub(super) fn process_sync_actions(&mut self, actions: Vec<SyncAction>) -> bool {
        let mut needs_redraw = false;
        for action in actions {
            match action {
                SyncAction::CreateTab { window_id } => {
                    crate::debug_info!("TMUX", "Sync: Create tab for window @{}", window_id);
                    self.handle_tmux_window_add(window_id);
                    needs_redraw = true;
                }
                SyncAction::CloseTab { tab_id } => {
                    crate::debug_info!("TMUX", "Sync: Close tab {}", tab_id);
                    // Note: TmuxSync already called unmap_window when producing this action,
                    // so we only need to close the tab and handle the last-tab case.
                    // The window-wide pane-owner map is tab-scoped: its entries
                    // for this tab die with it (they outlived the close before —
                    // card 01a122a3).
                    self.tmux_state.remove_tab_pane_mappings(tab_id);
                    // Fast path: a daemon tab's teardown must not drop its
                    // hidden shell's TerminalManager on the UI thread.
                    let was_last = self.tab_manager.close_tab_fast(tab_id);
                    if was_last {
                        self.handle_tmux_session_ended();
                    } else {
                        self.ensure_visible_active_mux_tab();
                    }
                    needs_redraw = true;
                }
                SyncAction::RenameTab { tab_id, name } => {
                    crate::debug_info!("TMUX", "Sync: Rename tab {} to '{}'", tab_id, name);
                    if let Some(tab) = self.tab_manager.get_tab_mut(tab_id) {
                        tab.set_title(&name);
                        needs_redraw = true;
                    }
                }
                SyncAction::UpdateLayout {
                    tab_id,
                    layout,
                    zoomed,
                } => {
                    crate::debug_info!("TMUX", "Sync: Update layout for tab {}", tab_id);
                    // Reverse-lookup the tmux window ID so handle_tmux_layout_change can use
                    // it for mapping and pane-tree reconciliation.
                    if let Some(window_id) = self.tmux_state.tmux_sync.get_window(tab_id) {
                        self.handle_tmux_layout_change(window_id, &layout, zoomed);
                        needs_redraw = true;
                    }
                }
                SyncAction::PaneOutput { pane_id, data } => {
                    crate::debug_trace!(
                        "TMUX",
                        "Sync: Route {} bytes to pane {}",
                        data.len(),
                        pane_id
                    );
                    // pane_id is already a native PaneId (translated by TmuxSync).
                    // Route directly to the pane's terminal across all tabs.
                    // try_lock: intentional — called from the sync event loop. On miss: this
                    // chunk of pane output is dropped. Acceptable; tmux backpressure via
                    // %pause/%continue handles flow control and recovery.
                    for tab in self.tab_manager.tabs_mut() {
                        if let Some(pane_manager) = tab.pane_manager_mut()
                            && let Some(pane) = pane_manager.get_pane_mut(pane_id)
                            && let Ok(term) = pane.terminal.try_read()
                        {
                            term.process_mux_output(&data);
                            break;
                        }
                    }
                }
                SyncAction::SessionEnded => {
                    crate::debug_info!("TMUX", "Sync: Session ended");
                    self.handle_tmux_session_ended();
                    needs_redraw = true;
                }
                SyncAction::Pause => {
                    crate::debug_info!("TMUX", "Sync: Pause");
                    self.handle_tmux_pause();
                }
                SyncAction::Continue => {
                    crate::debug_info!("TMUX", "Sync: Continue");
                    self.handle_tmux_continue();
                    needs_redraw = true;
                }
            }
        }
        needs_redraw
    }

    /// After a daemon window's tab closes, the tab manager's pick of the
    /// next active tab is index-based and may land on a HIDDEN mux tab
    /// (UX.md D7), leaving the window with nothing visible to render.
    /// Prefer a visible tab; with none left, re-show the picked one — the
    /// same successor rule the last-pane hide uses.
    fn ensure_visible_active_mux_tab(&mut self) {
        if self.tmux_state.transport.is_none() {
            return;
        }
        let Some(active) = self.tab_manager.active_tab() else {
            return;
        };
        if !active.is_hidden {
            return;
        }
        let active_id = active.id;
        match self
            .tab_manager
            .tabs()
            .iter()
            .find(|t| !t.is_hidden)
            .map(|t| t.id)
        {
            Some(visible) => self.tab_manager.switch_to(visible),
            None => {
                if let Some(tab) = self.tab_manager.get_tab_mut(active_id) {
                    tab.is_hidden = false;
                }
            }
        }
    }
}