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
//! Terminal-state snapshot for one render frame.
//!
//! `gather_render_data` is the first substantive step of every render cycle.
//! It assembles a `FrameRenderData` by coordinating helpers from three
//! focused sub-modules:
//!
//! - `viewport`: `gather_viewport_sizing`, `resolve_cursor_shader_hide`
//! - `tab_snapshot`: `extract_tab_cells` / `TabCellsSnapshot`
//! - (this module): URL detection, search highlights, scrollback marks,
//! window title update, cursor blink
//!
use super::FrameRenderData;
use super::tab_snapshot;
use crate::app::window_state::WindowState;
impl WindowState {
/// Gather all data needed for this render frame.
/// Returns None if rendering should be skipped (no renderer, no active tab, terminal locked, etc.)
pub(super) fn gather_render_data(&mut self) -> Option<FrameRenderData> {
let (renderer_size, visible_lines, _grid_cols) = self.gather_viewport_sizing()?;
// Get active tab's terminal and immediate state snapshots (avoid long borrows)
let (
terminal,
scroll_offset,
mouse_selection,
cache_cells,
cache_generation,
cache_scroll_offset,
cache_cursor_pos,
cache_selection,
cached_scrollback_len,
cache_grid_dims,
cached_terminal_title,
hovered_url,
) = match self.tab_manager.active_tab() {
Some(t) => (
// Use the focused pane's terminal for cache invalidation.
// In single-pane mode this is the same Arc as tab.terminal.
// In split-pane mode, using the primary pane's terminal means changes
// to a secondary focused pane never trigger a cache miss, so URL
// detection never re-runs and stale underlines persist after content
// changes or terminal clears in the focused pane.
t.pane_manager
.as_ref()
.and_then(|pm| pm.focused_pane())
.map(|p| p.terminal.clone())
.unwrap_or_else(|| t.terminal.clone()),
t.active_scroll_state().offset,
t.selection_mouse().selection,
t.active_cache().cells.clone(),
t.active_cache().generation,
t.active_cache().scroll_offset,
t.active_cache().cursor_pos,
t.active_cache().selection,
t.active_cache().scrollback_len,
t.active_cache().grid_dims,
t.active_cache().terminal_title.clone(),
t.active_mouse().hovered_url.clone(),
),
None => return None,
};
// Check if shell has exited
let _is_running = if let Ok(term) = terminal.try_write() {
term.is_running()
} else {
true // Assume running if locked
};
// Extract terminal cells using the focused tab_snapshot helper.
let was_alt_screen = self
.tab_manager
.active_tab()
.map(|t| t.was_alt_screen)
.unwrap_or(false);
let snap = self.extract_tab_cells(tab_snapshot::TabCellsParams {
scroll_offset,
mouse_selection,
cache_cells,
cache_generation,
cache_scroll_offset,
cache_cursor_pos,
cache_selection,
cache_grid_dims,
terminal: terminal.clone(),
was_alt_screen,
})?;
let mut cells = snap.cells;
let current_cursor_pos = snap.cursor_pos;
let cursor_style = snap.cursor_style;
let shader_cursor_pos = snap.shader_cursor_pos;
let shader_cursor_style = snap.shader_cursor_style;
let is_alt_screen = snap.is_alt_screen;
let current_generation = snap.current_generation;
let cell_grid_dims = snap.grid_dims;
if let Some(tab) = self.tab_manager.active_tab_mut() {
tab.was_alt_screen = is_alt_screen;
}
// Ensure cursor visibility flag for cell renderer reflects current config every frame
// (so toggling "Hide default cursor" takes effect immediately even if no other changes).
// Use the focused viewport helper to resolve hide-cursor state.
let hide_cursor_for_shader = self.resolve_cursor_shader_hide(is_alt_screen);
if let Some(renderer) = &mut self.renderer {
renderer.set_cursor_hidden_for_shader(hide_cursor_for_shader);
}
// Flush regenerated cells into the render cache (no-op on cache hit).
self.flush_cell_cache(&cells, current_cursor_pos, cell_grid_dims);
// Pre-populate the focused pane's cell cache so that gather_pane_render_data
// uses the SAME cells that URL detection saw. Only on cache-miss frames
// (fresh cells generated) — on cache-hit frames the cells are unchanged, so
// the clone is unnecessary and its cost (10K+ String clones for each Cell's
// grapheme) degrades FPS in long-running tmux sessions with many panes.
if !self.debug.cache_hit
&& let Some(tab) = self.tab_manager.active_tab_mut()
&& let Some(ref mut pm) = tab.pane_manager
&& let Some(pane) = pm.focused_pane_mut()
{
pane.cache.pane_cells = Some(std::sync::Arc::new(cells.clone()));
pane.cache.pane_cells_generation = current_generation;
pane.cache.pane_cells_scroll_offset = scroll_offset;
pane.cache.pane_cells_grid_dims = cell_grid_dims;
}
let mut show_scrollbar = self.should_show_scrollbar();
let (scrollback_len, terminal_title, shell_lifecycle_events) = self
.collect_scrollback_state(
&terminal,
current_cursor_pos,
cached_scrollback_len,
&cached_terminal_title,
);
// Fire CommandComplete alert sound for any finished commands.
if shell_lifecycle_events.iter().any(|e| {
matches!(
e,
par_term_terminal::ShellLifecycleEvent::CommandFinished { .. }
)
}) {
self.play_alert_sound(crate::config::AlertEvent::CommandComplete);
}
// Update cache scrollback and clamp scroll state.
//
// In pane mode the focused pane's own terminal holds the scrollback, not
// `tab.terminal`. Clamping here with `tab.terminal.scrollback_len()` would
// incorrectly cap (or zero-out) the scroll offset every frame. The correct
// clamp happens later in the pane render path once we know the focused pane's
// actual scrollback length.
let has_multiple_panes = self
.tab_manager
.active_tab()
.map(|t| t.has_multiple_panes())
.unwrap_or(false);
if let Some(tab) = self.tab_manager.active_tab_mut() {
// In multi-pane mode, tab.terminal may differ from the focused pane's
// terminal (e.g. after a split, the new pane has its own terminal).
// Writing tab.terminal's scrollback_len into the focused pane's cache
// would incorrectly show the original pane's scrollbar on the new pane.
// The correct per-pane scrollback_len is written later from
// gather_pane_render_data in gpu_submit.rs.
if !has_multiple_panes {
tab.active_cache_mut().scrollback_len = scrollback_len;
let sb_len = tab.active_cache().scrollback_len;
tab.active_scroll_state_mut().clamp_to_scrollback(sb_len);
}
}
// Keep copy mode dimensions in sync with terminal
if self.copy_mode.active
&& let Ok(term) = terminal.try_write()
{
let (cols, rows) = term.dimensions();
self.copy_mode.update_dimensions(cols, rows, scrollback_len);
}
let (scrollback_marks, marks_override_scrollbar) = self.collect_scrollback_marks(&terminal);
// Keep scrollbar visible when mark indicators exist AND there is scrollback
// to navigate. Without scrollback there is nothing to scroll to, and showing
// a scrollbar (with marks from the current prompt line) would be misleading
// and visually indistinguishable from marks that belong to a different tab.
// In multi-pane mode, `scrollback_len` comes from tab.terminal which may
// differ from the focused pane's terminal; skip this override and let the
// per-pane scrollbar logic (should_show_scrollbar) handle it.
if marks_override_scrollbar && scrollback_len > 0 && !has_multiple_panes {
show_scrollbar = true;
}
// Update window title if terminal has set one via OSC sequences.
self.update_window_title_if_changed(&terminal_title, &cached_terminal_title, &hovered_url);
// Total lines = visible lines + actual scrollback content
let total_lines = visible_lines + scrollback_len;
// Detect URLs, apply underlines, and apply search highlights.
let debug_url_detect_time = self.apply_url_and_search_highlights(
&mut cells,
&renderer_size,
cell_grid_dims,
scroll_offset,
scrollback_len,
visible_lines,
);
// Update cursor blink state
self.update_cursor_blink();
Some(FrameRenderData {
cells,
cursor_pos: current_cursor_pos,
cursor_style,
shader_cursor_pos,
shader_cursor_style,
is_alt_screen,
scrollback_len,
show_scrollbar,
visible_lines,
scrollback_marks,
total_lines,
debug_url_detect_time,
})
}
}