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
//! 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;
}
}
}
}
}