Skip to main content

par_term/app/tmux_handler/notifications/
session.rs

1//! Session-level tmux notification handlers.
2//!
3//! Covers session lifecycle events: session started/renamed/ended, client-size
4//! synchronization, and window-title management.
5
6use crate::app::window_state::WindowState;
7
8impl WindowState {
9    /// Handle session started notification
10    pub(super) fn handle_tmux_session_started(&mut self, session_name: &str) {
11        crate::debug_info!("TMUX", "Session started: {}", session_name);
12
13        // Store the session name for later use (e.g., window title updates)
14        self.tmux_state.tmux_session_name = Some(session_name.to_string());
15
16        // Update window title with session name: "par-term - [tmux: session_name]"
17        self.update_window_title_with_tmux();
18
19        // Check for automatic profile switching based on tmux session name
20        self.apply_tmux_session_profile(session_name);
21
22        // Update the gateway tab's title to show tmux session
23        if let Some(gateway_tab_id) = self.tmux_state.tmux_gateway_tab_id
24            && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
25        {
26            tab.set_title(&format!("[tmux: {}]", session_name));
27            crate::debug_info!(
28                "TMUX",
29                "Updated gateway tab {} title to '[tmux: {}]'",
30                gateway_tab_id,
31                session_name
32            );
33        }
34
35        // Enable sync now that session is connected
36        self.tmux_state.tmux_sync.enable();
37
38        // Note: tmux_gateway_active was already set on the gateway tab during initiate_tmux_gateway()
39
40        // Set window-size to 'smallest' so tmux respects par-term's size
41        // even when other (larger) clients are attached.
42        // This is critical for proper multi-client behavior.
43        let _ = self.write_to_gateway("set-option -g window-size smallest\n");
44        crate::debug_info!(
45            "TMUX",
46            "Set window-size to smallest for multi-client support"
47        );
48
49        // Tell tmux the terminal size so panes can be properly sized
50        // Without this, tmux uses a very small default and splits will fail
51        self.send_tmux_client_size();
52
53        // Note: Initial pane content comes from layout-change handling which sends Ctrl+L
54        // to each pane. We don't send Enter here as it would execute a command.
55
56        // Show success toast
57        self.show_toast(format!("tmux: Connected to session '{}'", session_name));
58    }
59
60    /// Send the terminal size to tmux so it knows the client dimensions
61    ///
62    /// In control mode, tmux doesn't know the terminal size unless we tell it.
63    /// Without this, tmux uses a very small default and pane splits will fail
64    /// with "no space for new pane".
65    pub(super) fn send_tmux_client_size(&self) {
66        // Get the terminal grid size from the renderer
67        if let Some(renderer) = &self.renderer {
68            let (cols, rows) = renderer.grid_size();
69            let cmd = crate::tmux::TmuxCommand::set_client_size(cols, rows);
70            let cmd_str = format!("{}\n", cmd.as_str());
71
72            if self.write_to_gateway(&cmd_str) {
73                crate::debug_trace!("TMUX", "Sent client size to tmux: {}x{}", cols, rows);
74            } else {
75                crate::debug_error!("TMUX", "Failed to send client size to tmux");
76            }
77        } else {
78            crate::debug_error!("TMUX", "Cannot send client size - no renderer available");
79        }
80    }
81
82    /// Notify tmux of a window/pane resize
83    ///
84    /// Called when the window is resized to keep tmux in sync with par-term's size.
85    /// This sends `refresh-client -C cols,rows` to tmux in gateway mode.
86    pub fn notify_tmux_of_resize(&self) {
87        // par-mux transport: push the new grid size and cell pixels through
88        // the daemon client; the gateway path below writes to the gateway
89        // PTY.
90        #[cfg(feature = "mux")]
91        if let Some(transport) = &self.tmux_state.transport
92            && let Some(renderer) = &self.renderer
93        {
94            let (cols, rows) = super::mux::mux_client_grid(renderer);
95            let cell_px = super::mux::mux_client_cell_px(renderer);
96            // mux_focused_pane is unset after a fresh attach until a click
97            // or focus push — resolve the focused pane the same way input
98            // routing does, or every resize is silently skipped and daemon
99            // panes keep the old geometry (broken wrapping after resize).
100            let focused = self.focused_mux_pane_from_native();
101            super::mux::push_client_size(&**transport, focused, cols, rows, cell_px);
102            return;
103        }
104
105        // Only send if tmux gateway is active
106        if !self.is_gateway_active() {
107            return;
108        }
109
110        self.send_tmux_client_size();
111    }
112
113    /// Update window title with tmux session info
114    /// Format: "window_title - [tmux: session_name]"
115    pub(crate) fn update_window_title_with_tmux(&self) {
116        let title = if let Some(session_name) = &self.tmux_state.tmux_session_name {
117            format!(
118                "{} - [tmux: {}]",
119                self.config.load().window_title,
120                session_name
121            )
122        } else {
123            self.config.load().window_title.clone()
124        };
125        let formatted = self.format_title(&title);
126        self.with_window(|w| w.set_title(&formatted));
127    }
128
129    /// Handle session renamed notification
130    pub(super) fn handle_tmux_session_renamed(&mut self, session_name: &str) {
131        crate::debug_info!("TMUX", "Session renamed to: {}", session_name);
132
133        // Update stored session name
134        self.tmux_state.tmux_session_name = Some(session_name.to_string());
135
136        // Update window title with new session name
137        self.update_window_title_with_tmux();
138    }
139
140    /// Handle session ended notification
141    pub(super) fn handle_tmux_session_ended(&mut self) {
142        crate::debug_info!("TMUX", "Session ended");
143
144        // Restore gateway tab visibility before tearing down tmux state
145        self.show_gateway_tab();
146
147        // Collect tmux display tabs to close (tabs with tmux_pane_id set, excluding gateway)
148        let gateway_tab_id = self.tmux_state.tmux_gateway_tab_id;
149        let tmux_tabs_to_close: Vec<crate::tab::TabId> = self
150            .tab_manager
151            .tabs()
152            .iter()
153            .filter_map(|tab| {
154                // Close tabs that were displaying daemon content: a
155                // layout set their tmux_pane_id, or they are still mapped
156                // to a daemon window. The second test matters for a window
157                // that dies before its first %layout-change — no pane id
158                // was ever set, and without the mapping check its tab
159                // outlived the session as an immortal dead tab.
160                if (tab.tmux.tmux_pane_id.is_some()
161                    || self.tmux_state.tmux_sync.get_window(tab.id).is_some())
162                    && Some(tab.id) != gateway_tab_id
163                {
164                    Some(tab.id)
165                } else {
166                    None
167                }
168            })
169            .collect();
170
171        // Close tmux display tabs. The fast path pre-kills the hidden
172        // shells and releases their terminal Arcs on background threads —
173        // dropping them inline waits on each reader thread (up to 2 s per
174        // tab) on the UI thread.
175        for tab_id in tmux_tabs_to_close {
176            crate::debug_info!("TMUX", "Closing tmux display tab {}", tab_id);
177            let _ = self.tab_manager.close_tab_fast(tab_id);
178        }
179
180        // Disable tmux control mode on the gateway tab and clear auto-applied profile
181        if let Some(gateway_tab_id) = self.tmux_state.tmux_gateway_tab_id
182            && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
183            && tab.tmux.tmux_gateway_active
184        {
185            tab.tmux.tmux_gateway_active = false;
186            tab.tmux.tmux_pane_id = None;
187            tab.clear_auto_profile(); // Clear tmux session profile
188            // try_lock: intentional — session-ended cleanup runs from the sync event loop
189            // where blocking would stall the entire GUI.
190            // On miss: set the deferred flag so the notification poll loop retries on the
191            // next frame, guaranteeing the terminal parser eventually exits control mode.
192            if let Ok(term) = tab.terminal.try_read() {
193                term.set_tmux_control_mode(false);
194            } else {
195                crate::debug_error!(
196                    "TAB",
197                    "session-ended: could not acquire terminal lock to disable tmux control mode \
198                     on tab {} — deferring to next poll cycle",
199                    gateway_tab_id
200                );
201                tab.tmux.pending_tmux_mode_disable = true;
202            }
203        }
204        self.tmux_state.tmux_gateway_tab_id = None;
205
206        // Clean up tmux session state
207        if let Some(mut session) = self.tmux_state.tmux_session.take() {
208            session.disconnect();
209        }
210        self.tmux_state.tmux_session_name = None;
211        self.tmux_state.mux_session_id = None;
212
213        // Clear pane mappings
214        self.tmux_state.tmux_pane_owners.clear();
215
216        // Reset window title (now without tmux info)
217        self.update_window_title_with_tmux();
218
219        // Clear sync state
220        self.tmux_state.tmux_sync = crate::tmux::TmuxSync::new();
221
222        // Show toast
223        self.show_toast("tmux: Session ended");
224    }
225}