1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
//! Tmux notification polling — drains, converts and routes control-mode events.
//!
//! `check_tmux_notifications` is called each frame from `about_to_wait`.
//! It pulls raw notifications from the gateway terminal's parser, converts
//! them via `ParserBridge`, then dispatches each to the appropriate handler
//! grouped by type (session/window → layout → output → other).
use crate::app::window_state::WindowState;
use crate::tmux::{ParserBridge, TmuxNotification};
impl WindowState {
/// Retry any deferred `set_tmux_control_mode(false)` calls on all tabs.
///
/// When `handle_tmux_session_ended` cannot acquire the terminal lock via `try_lock()`
/// it sets `tab.tmux.pending_tmux_mode_disable = true`. This function is called each frame
/// from `check_tmux_notifications` and clears the flag as soon as the lock becomes
/// available, ensuring the terminal parser eventually exits tmux control mode.
fn retry_pending_tmux_mode_disable(&mut self) {
for tab in self.tab_manager.tabs_mut() {
if !tab.tmux.pending_tmux_mode_disable {
continue;
}
// try_lock: intentional — we are in the sync event loop. On miss: leave the
// flag set and retry next frame. The lock will be free once the PTY reader
// finishes its current read (which is short-lived).
if let Ok(term) = tab.terminal.try_write() {
term.set_tmux_control_mode(false);
tab.tmux.pending_tmux_mode_disable = false;
crate::debug_info!(
"TAB",
"Deferred tmux control mode disable applied to tab {}",
tab.id
);
}
}
}
/// Poll and process tmux notifications from the control mode session.
///
/// In gateway mode, notifications come from the terminal's tmux control parser
/// rather than a separate channel. This should be called in about_to_wait.
///
/// Returns true if any notifications were processed (triggers redraw).
pub(crate) fn check_tmux_notifications(&mut self) -> bool {
// Early exit if tmux integration is disabled
if !self.config.tmux_enabled {
return false;
}
// Deferred tmux-control-mode disable: retry on each frame until the lock is
// available. This resolves the case where `handle_tmux_session_ended` could not
// acquire the terminal lock at cleanup time, leaving the parser in control mode.
self.retry_pending_tmux_mode_disable();
// Check if we have an active gateway session
let _session = match &self.tmux_state.tmux_session {
Some(s) if s.is_gateway_active() => s,
_ => return false,
};
// Get the gateway tab ID - this is where the tmux control connection lives
let gateway_tab_id = match self.tmux_state.tmux_gateway_tab_id {
Some(id) => id,
None => return false,
};
// Drain notifications from the gateway tab's terminal tmux parser
let core_notifications = if let Some(tab) = self.tab_manager.get_tab(gateway_tab_id) {
// try_lock: intentional — called from the sync event loop (about_to_wait) where
// blocking would stall the entire GUI. On miss: returns false (no notifications
// processed this frame); they will be picked up on the next poll cycle.
if let Ok(term) = tab.terminal.try_write() {
term.drain_tmux_notifications()
} else {
return false;
}
} else {
return false;
};
if core_notifications.is_empty() {
return false;
}
// Log all raw core notifications for debugging
for (i, notif) in core_notifications.iter().enumerate() {
crate::debug_info!(
"TMUX",
"Core notification {}/{}: {:?}",
i + 1,
core_notifications.len(),
notif
);
}
// Convert core notifications to frontend notifications
let notifications = ParserBridge::convert_all(core_notifications);
if notifications.is_empty() {
crate::debug_trace!(
"TMUX",
"All core notifications were filtered out by parser bridge"
);
return false;
}
crate::debug_info!(
"TMUX",
"Processing {} tmux notifications (gateway mode)",
notifications.len()
);
let mut needs_redraw = false;
// First, update gateway state based on notifications
for notification in ¬ifications {
crate::debug_trace!("TMUX", "Processing notification: {:?}", notification);
if let Some(session) = &mut self.tmux_state.tmux_session
&& session.process_gateway_notification(notification)
{
crate::debug_info!(
"TMUX",
"State transition - gateway_state: {:?}, session_state: {:?}",
session.gateway_state(),
session.state()
);
needs_redraw = true;
}
}
// Separate notifications into two buckets:
// • direct — handled by dedicated handlers (TmuxSync cannot translate these)
// • sync — routed through TmuxSync for ID translation, then dispatched via
// process_sync_actions in priority order: session → layout → output → other
//
// Processing in groups preserves the ordering guarantee: session/window structure
// is set up (and window→tab mappings created) before layout changes are applied,
// and pane mappings from layout are available before output arrives.
let mut direct_notifications = Vec::new();
let mut session_sync = Vec::new();
let mut layout_sync = Vec::new();
let mut output_sync = Vec::new();
let mut other_sync = Vec::new();
for notification in notifications {
match ¬ification {
TmuxNotification::ControlModeStarted
| TmuxNotification::SessionStarted(_)
| TmuxNotification::SessionRenamed(_)
| TmuxNotification::PaneFocusChanged { .. }
| TmuxNotification::Error(_) => {
direct_notifications.push(notification);
}
TmuxNotification::WindowAdd(_)
| TmuxNotification::WindowClose(_)
| TmuxNotification::WindowRenamed { .. }
| TmuxNotification::SessionEnded => {
session_sync.push(notification);
}
TmuxNotification::LayoutChange { .. } => {
layout_sync.push(notification);
}
TmuxNotification::Output { .. } => {
output_sync.push(notification);
}
TmuxNotification::Pause | TmuxNotification::Continue => {
other_sync.push(notification);
}
}
}
// --- Direct dispatch (notifications TmuxSync does not handle) ---
for notification in direct_notifications {
match notification {
TmuxNotification::ControlModeStarted => {
crate::debug_info!("TMUX", "Control mode started - tmux is ready");
}
TmuxNotification::SessionStarted(session_name) => {
self.handle_tmux_session_started(&session_name);
needs_redraw = true;
}
TmuxNotification::SessionRenamed(session_name) => {
self.handle_tmux_session_renamed(&session_name);
needs_redraw = true;
}
TmuxNotification::Error(msg) => {
self.handle_tmux_error(&msg);
}
TmuxNotification::PaneFocusChanged { pane_id } => {
self.handle_tmux_pane_focus_changed(pane_id);
needs_redraw = true;
}
_ => {}
}
}
// --- TmuxSync dispatch: group 1 — session/window structure ---
// Creates window→tab mappings; must run before layout and output.
let session_actions = self
.tmux_state
.tmux_sync
.process_notifications(&session_sync);
needs_redraw |= self.process_sync_actions(session_actions);
// --- TmuxSync dispatch: group 2 — layout changes ---
// Applies pane layout; requires window mappings from group 1.
let layout_actions = self
.tmux_state
.tmux_sync
.process_notifications(&layout_sync);
needs_redraw |= self.process_sync_actions(layout_actions);
// Fallback: LayoutChange notifications that TmuxSync could not translate
// (no window→tab mapping yet). The direct handler handles on-the-fly mapping.
for notification in &layout_sync {
if let TmuxNotification::LayoutChange { window_id, layout } = notification
&& self.tmux_state.tmux_sync.get_tab(*window_id).is_none()
{
self.handle_tmux_layout_change(*window_id, layout);
needs_redraw = true;
}
}
// --- TmuxSync dispatch: group 3 — pane output ---
// Routes bytes to native panes; requires pane mappings from group 2.
let output_actions = self
.tmux_state
.tmux_sync
.process_notifications(&output_sync);
needs_redraw |= self.process_sync_actions(output_actions);
// Fallback: Output for panes not yet mapped. The direct handler has multi-level
// fallback logic (tab-level routing, on-demand tab creation).
for notification in output_sync {
if let TmuxNotification::Output { pane_id, data } = notification
&& self.tmux_state.tmux_sync.get_native_pane(pane_id).is_none()
{
self.handle_tmux_output(pane_id, &data);
needs_redraw = true;
}
}
// --- TmuxSync dispatch: group 4 — flow control (pause/continue) ---
let other_actions = self.tmux_state.tmux_sync.process_notifications(&other_sync);
needs_redraw |= self.process_sync_actions(other_actions);
needs_redraw
}
}