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 through the daemon
88 // client; the gateway path below writes to the gateway PTY.
89 #[cfg(feature = "mux")]
90 if let Some(transport) = &self.tmux_state.transport
91 && let Some(renderer) = &self.renderer
92 {
93 let (cols, rows) = super::mux::mux_client_grid(renderer);
94 // mux_focused_pane is unset after a fresh attach until a click
95 // or focus push — resolve the focused pane the same way input
96 // routing does, or every resize is silently skipped and daemon
97 // panes keep the old geometry (broken wrapping after resize).
98 let focused = self.focused_mux_pane_from_native();
99 super::mux::push_client_size(&**transport, focused, cols, rows);
100 return;
101 }
102
103 // Only send if tmux gateway is active
104 if !self.is_gateway_active() {
105 return;
106 }
107
108 self.send_tmux_client_size();
109 }
110
111 /// Update window title with tmux session info
112 /// Format: "window_title - [tmux: session_name]"
113 pub(crate) fn update_window_title_with_tmux(&self) {
114 let title = if let Some(session_name) = &self.tmux_state.tmux_session_name {
115 format!(
116 "{} - [tmux: {}]",
117 self.config.load().window_title,
118 session_name
119 )
120 } else {
121 self.config.load().window_title.clone()
122 };
123 let formatted = self.format_title(&title);
124 self.with_window(|w| w.set_title(&formatted));
125 }
126
127 /// Handle session renamed notification
128 pub(super) fn handle_tmux_session_renamed(&mut self, session_name: &str) {
129 crate::debug_info!("TMUX", "Session renamed to: {}", session_name);
130
131 // Update stored session name
132 self.tmux_state.tmux_session_name = Some(session_name.to_string());
133
134 // Update window title with new session name
135 self.update_window_title_with_tmux();
136 }
137
138 /// Handle session ended notification
139 pub(super) fn handle_tmux_session_ended(&mut self) {
140 crate::debug_info!("TMUX", "Session ended");
141
142 // Restore gateway tab visibility before tearing down tmux state
143 self.show_gateway_tab();
144
145 // Collect tmux display tabs to close (tabs with tmux_pane_id set, excluding gateway)
146 let gateway_tab_id = self.tmux_state.tmux_gateway_tab_id;
147 let tmux_tabs_to_close: Vec<crate::tab::TabId> = self
148 .tab_manager
149 .tabs()
150 .iter()
151 .filter_map(|tab| {
152 // Close tabs that were displaying tmux content (have tmux_pane_id)
153 // but not the gateway tab itself
154 if tab.tmux.tmux_pane_id.is_some() && Some(tab.id) != gateway_tab_id {
155 Some(tab.id)
156 } else {
157 None
158 }
159 })
160 .collect();
161
162 // Close tmux display tabs
163 for tab_id in tmux_tabs_to_close {
164 crate::debug_info!("TMUX", "Closing tmux display tab {}", tab_id);
165 let _ = self.tab_manager.close_tab(tab_id);
166 }
167
168 // Disable tmux control mode on the gateway tab and clear auto-applied profile
169 if let Some(gateway_tab_id) = self.tmux_state.tmux_gateway_tab_id
170 && let Some(tab) = self.tab_manager.get_tab_mut(gateway_tab_id)
171 && tab.tmux.tmux_gateway_active
172 {
173 tab.tmux.tmux_gateway_active = false;
174 tab.tmux.tmux_pane_id = None;
175 tab.clear_auto_profile(); // Clear tmux session profile
176 // try_lock: intentional — session-ended cleanup runs from the sync event loop
177 // where blocking would stall the entire GUI.
178 // On miss: set the deferred flag so the notification poll loop retries on the
179 // next frame, guaranteeing the terminal parser eventually exits control mode.
180 if let Ok(term) = tab.terminal.try_read() {
181 term.set_tmux_control_mode(false);
182 } else {
183 crate::debug_error!(
184 "TAB",
185 "session-ended: could not acquire terminal lock to disable tmux control mode \
186 on tab {} — deferring to next poll cycle",
187 gateway_tab_id
188 );
189 tab.tmux.pending_tmux_mode_disable = true;
190 }
191 }
192 self.tmux_state.tmux_gateway_tab_id = None;
193
194 // Clean up tmux session state
195 if let Some(mut session) = self.tmux_state.tmux_session.take() {
196 session.disconnect();
197 }
198 self.tmux_state.tmux_session_name = None;
199
200 // Clear pane mappings
201 self.tmux_state.tmux_pane_to_native_pane.clear();
202 self.tmux_state.native_pane_to_tmux_pane.clear();
203
204 // Reset window title (now without tmux info)
205 self.update_window_title_with_tmux();
206
207 // Clear sync state
208 self.tmux_state.tmux_sync = crate::tmux::TmuxSync::new();
209
210 // Show toast
211 self.show_toast("tmux: Session ended");
212 }
213}