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
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
//! Layout-change tmux notification handler.
//!
//! `handle_tmux_layout_change` parses the tmux layout string, reconciles it
//! with the current native pane tree, and delegates to one of four case
//! handlers in `layout_apply`:
//!
//! - same panes (geometry only)
//! - panes removed
//! - panes added
//! - full recreation
//!
//! `request_pane_refresh` and `log_layout_node` are helpers used exclusively
//! by the layout handler.
use crate::app::window_state::WindowState;
use crate::tmux::{TmuxLayout, TmuxWindowId};
/// Render bounds info passed through layout helper methods.
///
/// Fields: (physical_size, scale_factor, viewport_x, viewport_y, cell_width, cell_height, line_height)
pub(super) type BoundsInfo = Option<(winit::dpi::PhysicalSize<u32>, f32, f32, f32, f32, f32, f32)>;
impl WindowState {
/// Request content refresh for specific panes
///
/// After learning about panes from a layout change, we need to trigger
/// each pane to send its content. tmux only sends %output for NEW content,
/// not existing screen content when attaching.
///
/// We use two approaches:
/// 1. Send Ctrl+L (C-l) to each pane, which triggers shell screen redraw
/// 2. Use capture-pane -p to get the current pane content (comes as command response)
pub(super) fn request_pane_refresh(&self, pane_ids: &[crate::tmux::TmuxPaneId]) {
for pane_id in pane_ids {
// Approach 1: Send Ctrl+L (screen redraw signal) to trigger shell to repaint
// This works for interactive shells that respond to SIGWINCH-like events
let cmd = format!("send-keys -t %{} C-l\n", pane_id);
if self.write_to_gateway(&cmd) {
crate::debug_trace!("TMUX", "Sent C-l to pane %{} for refresh", pane_id);
}
}
// Request client refresh which may help with layout sync
let refresh_cmd = "refresh-client\n";
if self.write_to_gateway(refresh_cmd) {
crate::debug_info!(
"TMUX",
"Requested client refresh for {} panes",
pane_ids.len()
);
}
}
/// Handle layout change notification - updates pane arrangement
pub(super) fn handle_tmux_layout_change(&mut self, window_id: TmuxWindowId, layout_str: &str) {
crate::debug_info!(
"TMUX",
"Layout changed for window @{}: {}",
window_id,
layout_str
);
// Parse the layout string
let parsed_layout = match TmuxLayout::parse(layout_str) {
Some(layout) => layout,
None => {
crate::debug_error!(
"TMUX",
"Failed to parse layout string for window @{}: {}",
window_id,
layout_str
);
return;
}
};
// Log the parsed layout structure
let pane_ids = parsed_layout.pane_ids();
crate::debug_info!(
"TMUX",
"Parsed layout for window @{}: {} panes (IDs: {:?})",
window_id,
pane_ids.len(),
pane_ids
);
// Log the layout structure for debugging
Self::log_layout_node(&parsed_layout.root, 0);
// Update focused pane in session if we have one
if !pane_ids.is_empty()
&& let Some(session) = &mut self.tmux_state.tmux_session
{
// Default to first pane if no focused pane set
if session.focused_pane().is_none() {
session.set_focused_pane(Some(pane_ids[0]));
}
}
// Find the corresponding tab and create window mapping if needed
let tab_id = if let Some(id) = self.tmux_state.tmux_sync.get_tab(window_id) {
Some(id)
} else {
// No window mapping exists - try to find a tab that has one of our panes
// This happens when we connect to an existing session and receive layout before window-add
let mut found_tab_id = None;
for pane_id in &pane_ids {
// Check if any tab has this tmux_pane_id set
for tab in self.tab_manager.tabs() {
if tab.tmux.tmux_pane_id == Some(*pane_id) {
found_tab_id = Some(tab.id);
crate::debug_info!(
"TMUX",
"Found existing tab {} with pane %{} for window @{}",
tab.id,
pane_id,
window_id
);
break;
}
}
if found_tab_id.is_some() {
break;
}
}
// If we found a tab, create the window mapping
if let Some(tid) = found_tab_id {
self.tmux_state.tmux_sync.map_window(window_id, tid);
crate::debug_info!(
"TMUX",
"Created window mapping: @{} -> tab {}",
window_id,
tid
);
}
found_tab_id
};
// Get bounds info from renderer for proper pane sizing (needed for both paths)
// Calculate status bar height for proper content area
let is_tmux_connected = self.is_tmux_connected();
let status_bar_height =
crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&self.config, is_tmux_connected);
let custom_status_bar_height = self.status_bar_ui.height(&self.config, self.is_fullscreen);
let bounds_info = self.renderer.as_ref().map(|r| {
let size = r.size();
let padding = r.window_padding();
let content_offset_y = r.content_offset_y();
let content_inset_right = r.content_inset_right();
let cell_width = r.cell_width();
let cell_height = r.cell_height();
// Scale status_bar_height from logical to physical pixels
let physical_status_bar_height =
(status_bar_height + custom_status_bar_height) * r.scale_factor();
(
size,
padding,
content_offset_y,
content_inset_right,
cell_width,
cell_height,
physical_status_bar_height,
)
});
if let Some(tab_id) = tab_id {
self.apply_layout_to_existing_tab(
tab_id,
window_id,
&parsed_layout,
&pane_ids,
bounds_info,
);
} else {
// No tab mapping found - create a new tab for this tmux window
self.create_tab_for_layout(window_id, &parsed_layout, &pane_ids, bounds_info);
}
}
/// Apply a parsed tmux layout to an already-mapped tab.
///
/// Handles four cases in priority order — delegates to helpers in `layout_apply`:
/// 1. Same panes — preserve terminals, update layout structure.
/// 2. Panes removed — incrementally close removed native panes, update layout.
/// 3. Panes added — rebuild tree preserving existing terminals, add new ones.
/// 4. Full recreation — completely replace the pane tree.
fn apply_layout_to_existing_tab(
&mut self,
tab_id: crate::tab::TabId,
window_id: TmuxWindowId,
parsed_layout: &TmuxLayout,
pane_ids: &[crate::tmux::TmuxPaneId],
bounds_info: BoundsInfo,
) {
crate::debug_info!(
"TMUX",
"Layout change for window @{} on tab {} - {} panes: {:?}",
window_id,
tab_id,
pane_ids.len(),
pane_ids
);
let Some(tab) = self.tab_manager.get_tab_mut(tab_id) else {
return;
};
// Initialize pane manager if needed
tab.init_pane_manager();
// Set pane bounds before applying layout
if let Some((
size,
padding,
content_offset_y,
content_inset_right,
_cell_width,
_cell_height,
status_bar_height,
)) = bounds_info
&& let Some(pm) = tab.pane_manager_mut()
{
// Tmux layouts always have multiple panes; hide window padding if configured
let effective_padding = if self.config.hide_window_padding_on_split {
0.0
} else {
padding
};
let content_width = size.width as f32 - effective_padding * 2.0 - content_inset_right;
let content_height =
size.height as f32 - content_offset_y - effective_padding - status_bar_height;
let bounds = crate::pane::PaneBounds::new(
effective_padding,
content_offset_y,
content_width,
content_height,
);
pm.set_bounds(bounds);
crate::debug_info!(
"TMUX",
"Set pane manager bounds: {}x{} at ({}, {})",
content_width,
content_height,
effective_padding,
content_offset_y
);
}
// Compute set deltas between existing and new tmux pane IDs
let existing_tmux_ids: std::collections::HashSet<_> = self
.tmux_state
.tmux_pane_to_native_pane
.keys()
.copied()
.collect();
let new_tmux_ids: std::collections::HashSet<_> = pane_ids.iter().copied().collect();
if existing_tmux_ids == new_tmux_ids && !existing_tmux_ids.is_empty() {
// Same panes - preserve terminals but update layout structure
self.handle_same_pane_layout_update(tab_id, parsed_layout, bounds_info);
return;
}
let panes_to_keep: std::collections::HashSet<_> = existing_tmux_ids
.intersection(&new_tmux_ids)
.copied()
.collect();
let panes_to_remove: Vec<_> = existing_tmux_ids
.difference(&new_tmux_ids)
.copied()
.collect();
let panes_to_add: Vec<_> = new_tmux_ids
.difference(&existing_tmux_ids)
.copied()
.collect();
if !panes_to_keep.is_empty() && !panes_to_remove.is_empty() && panes_to_add.is_empty() {
self.handle_pane_removal(
tab_id,
parsed_layout,
&panes_to_keep,
&panes_to_remove,
bounds_info,
);
return;
}
if !panes_to_keep.is_empty() && !panes_to_add.is_empty() && panes_to_remove.is_empty() {
self.handle_pane_addition(
tab_id,
parsed_layout,
&panes_to_keep,
&panes_to_add,
bounds_info,
);
return;
}
// Full layout recreation needed (complete replacement or complex changes)
self.handle_full_layout_recreation(tab_id, window_id, parsed_layout, pane_ids, bounds_info);
}
/// Log a layout node and its children recursively for debugging
pub(super) fn log_layout_node(node: &crate::tmux::LayoutNode, depth: usize) {
let indent = " ".repeat(depth);
match node {
crate::tmux::LayoutNode::Pane {
id,
width,
height,
x,
y,
} => {
crate::debug_trace!(
"TMUX",
"{}Pane %{}: {}x{} at ({}, {})",
indent,
id,
width,
height,
x,
y
);
}
crate::tmux::LayoutNode::VerticalSplit {
width,
height,
x,
y,
children,
} => {
crate::debug_trace!(
"TMUX",
"{}VerticalSplit: {}x{} at ({}, {}) with {} children",
indent,
width,
height,
x,
y,
children.len()
);
for child in children {
Self::log_layout_node(child, depth + 1);
}
}
crate::tmux::LayoutNode::HorizontalSplit {
width,
height,
x,
y,
children,
} => {
crate::debug_trace!(
"TMUX",
"{}HorizontalSplit: {}x{} at ({}, {}) with {} children",
indent,
width,
height,
x,
y,
children.len()
);
for child in children {
Self::log_layout_node(child, depth + 1);
}
}
}
}
}