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
use crate::app::window_state::WindowState;
use std::sync::Arc;
impl WindowState {
/// Send mouse event to terminal if mouse tracking is enabled
///
/// Returns true if event was consumed by terminal (mouse tracking enabled or alt screen active),
/// false otherwise. When on alt screen, we don't want local text selection.
///
/// In split pane mode, this routes events to the focused pane's terminal with
/// pane-relative cell coordinates.
pub(crate) fn try_send_mouse_event(&self, button: u8, pressed: bool) -> bool {
let tab = if let Some(t) = self.tab_manager.active_tab() {
t
} else {
return false;
};
let mouse_position = tab.active_mouse().position;
// Get the correct terminal, cell coordinates, and (for tmux routing) native pane ID
let (terminal_arc, col, row, native_pane_id) = if let Some(ref pm) = tab.pane_manager
&& let Some(focused_pane) = pm.focused_pane()
{
// Split pane mode: use focused pane's terminal with pane-relative coordinates
let Some((col, row)) =
self.pixel_to_pane_cell(mouse_position.0, mouse_position.1, &focused_pane.bounds)
else {
return false;
};
(
Arc::clone(&focused_pane.terminal),
col,
row,
Some(focused_pane.id),
)
} else {
// Single pane: use tab's terminal with global coordinates
let Some((col, row)) = self.pixel_to_cell(mouse_position.0, mouse_position.1) else {
return false;
};
(Arc::clone(&tab.terminal), col, row, None)
};
// try_lock: intentional — mouse button handler runs in the sync event loop.
// On miss: the mouse event is not forwarded to mouse-tracking apps this click.
// The user can click again; no data is permanently lost.
let Ok(term) = terminal_arc.try_write() else {
return false;
};
// Check if alternate screen is active - don't do local selection on alt screen
// even if mouse tracking isn't enabled (e.g., some TUI apps don't enable mouse)
let alt_screen_active = term.is_alt_screen_active();
// Check if mouse tracking is enabled
if term.is_mouse_tracking_enabled() {
// Encode mouse event
let encoded = term.encode_mouse_event(button, col, row, pressed, 0);
// Release the write lock before any I/O so we don't hold it across awaits
drop(term);
if !encoded.is_empty() {
// For tmux display panes: route via the gateway so the TUI app running
// inside the real tmux pane actually receives the mouse event. Writing
// to the local virtual terminal (no PTY) silently drops the bytes.
if self.is_tmux_connected()
&& let Some(native_id) = native_pane_id
&& let Some(&tmux_pane_id) =
self.tmux_state.native_pane_to_tmux_pane.get(&native_id)
{
let escaped = crate::tmux::escape_keys_for_tmux(&encoded);
let cmd = format!("send-keys -t %{} {}\n", tmux_pane_id, escaped);
self.write_to_gateway(&cmd);
} else {
// Non-tmux path: write directly to the local PTY
let terminal_clone = Arc::clone(&terminal_arc);
let runtime = Arc::clone(&self.runtime);
runtime.spawn(async move {
let t = terminal_clone.write().await;
let _ = t.write(&encoded);
});
}
}
return true; // Event consumed by mouse tracking
}
// On alt screen without mouse tracking - still consume event to prevent selection
if alt_screen_active {
return true;
}
false // Event not consumed, handle normally
}
pub(crate) fn active_terminal_mouse_tracking_enabled_at(
&self,
mouse_position: (f64, f64),
) -> bool {
let Some(tab) = self.tab_manager.active_tab() else {
return false;
};
if let Some(ref pm) = tab.pane_manager
&& let Some(focused_pane) = pm.focused_pane()
{
if self
.pixel_to_pane_cell(mouse_position.0, mouse_position.1, &focused_pane.bounds)
.is_none()
{
return false;
}
// try_lock: intentional — querying mouse tracking state from the sync event loop.
// On miss: returns false (no mouse tracking), which may cause a missed mouse
// event routing. The next mouse move/click will re-query correctly.
return focused_pane
.terminal
.try_write()
.ok()
.is_some_and(|term| term.is_mouse_tracking_enabled());
}
if self
.pixel_to_cell(mouse_position.0, mouse_position.1)
.is_none()
{
return false;
}
// try_lock: intentional — querying mouse tracking state for the single-pane path.
// On miss: returns false (no tracking detected). Cosmetically incorrect for this
// event only; the next query will succeed.
tab.terminal
.try_write()
.ok()
.is_some_and(|term| term.is_mouse_tracking_enabled())
}
}