par_term/app/tmux_handler/gateway_input.rs
1//! tmux input routing: send/paste to tmux sessions and prefix key handling.
2
3use crate::app::window_state::WindowState;
4
5impl WindowState {
6 /// Send input through tmux gateway mode.
7 ///
8 /// When in gateway mode, keyboard input is sent via `send-keys` command
9 /// written to the gateway tab's PTY. This routes input to the appropriate tmux pane.
10 ///
11 /// Returns true if input was handled via tmux, false if it should go to PTY directly.
12 pub fn send_input_via_tmux(&self, data: &[u8]) -> bool {
13 // Check if tmux is enabled and connected
14 if !self.config.load().tmux.tmux_enabled || !self.is_tmux_connected() {
15 crate::debug_trace!(
16 "TMUX",
17 "send_input_via_tmux: not sending - enabled={}, connected={}",
18 self.config.load().tmux.tmux_enabled,
19 self.is_tmux_connected()
20 );
21 return false;
22 }
23
24 let session = match &self.tmux_state.tmux_session {
25 Some(s) => s,
26 None => return false,
27 };
28
29 // Format the send-keys command - try pane-specific first
30 let cmd = match session.format_send_keys(data) {
31 Some(c) => {
32 crate::debug_trace!("TMUX", "Using pane-specific send-keys: {}", c.trim());
33 c
34 }
35 None => {
36 crate::debug_trace!("TMUX", "No focused pane for send-keys, trying window-based");
37 // No focused pane - try window-based routing
38 if let Some(cmd) = self.format_send_keys_for_window(data) {
39 crate::debug_trace!("TMUX", "Using window-based send-keys: {}", cmd.trim());
40 cmd
41 } else {
42 // No window mapping either - use untargeted send-keys
43 // This sends to tmux's currently active pane
44 let escaped = crate::tmux::escape_keys_for_tmux(data);
45 format!("send-keys {}\n", escaped)
46 }
47 }
48 };
49
50 // Write the command to the gateway tab's PTY
51 if self.write_to_gateway(&cmd) {
52 crate::debug_trace!("TMUX", "Sent {} bytes via gateway send-keys", data.len());
53 return true;
54 }
55
56 false
57 }
58
59 /// Send raw bytes to the focused tmux pane as literal input, bypassing tmux's
60 /// key-name interpretation and any per-pane modifyOtherKeys / extended-keys
61 /// re-encoding.
62 ///
63 /// Uses `send-keys -H`, which tags each byte as `KEYC_LITERAL`. tmux writes
64 /// these bytes straight to the pane's PTY via `bufferevent_write`, skipping
65 /// the `input_key()` encoder entirely — so whatever we put in arrives
66 /// unchanged at the inner application.
67 ///
68 /// Used for cases where the normal `send-keys` path would mangle the bytes,
69 /// e.g. Shift+Enter: the iTerm2 convention is to send raw LF (0x0a), but
70 /// `escape_keys_for_tmux` translates that to `C-j`, and tmux's
71 /// modifyOtherKeys-mode-2 encoder then delivers `\x1b[27;5;106~` instead of
72 /// the literal newline the application expects.
73 pub fn send_literal_bytes_via_tmux(&self, bytes: &[u8]) -> bool {
74 if !self.config.load().tmux.tmux_enabled || !self.is_tmux_connected() {
75 crate::debug_info!(
76 "SHIFTENTER",
77 "send_literal_bytes_via_tmux: refused - enabled={}, connected={}",
78 self.config.load().tmux.tmux_enabled,
79 self.is_tmux_connected(),
80 );
81 return false;
82 }
83
84 let session = match &self.tmux_state.tmux_session {
85 Some(s) => s,
86 None => {
87 crate::debug_info!("SHIFTENTER", "send_literal_bytes_via_tmux: no tmux_session");
88 return false;
89 }
90 };
91
92 let focused = session.focused_pane();
93 let cmd = match session.format_send_hex_keys(bytes) {
94 Some(c) => c,
95 None => {
96 crate::debug_info!(
97 "SHIFTENTER",
98 "format_send_hex_keys returned None (focused_pane={:?}, state={:?}, gateway={:?})",
99 focused,
100 session.state(),
101 session.gateway_state(),
102 );
103 return false;
104 }
105 };
106
107 crate::debug_info!(
108 "SHIFTENTER",
109 "writing to gateway (focused_pane={:?}): {:?}",
110 focused,
111 cmd.trim_end(),
112 );
113
114 if self.write_to_gateway(&cmd) {
115 crate::debug_info!(
116 "SHIFTENTER",
117 "send_literal_bytes_via_tmux: success ({} bytes)",
118 bytes.len()
119 );
120 return true;
121 }
122
123 crate::debug_info!("SHIFTENTER", "write_to_gateway failed");
124 false
125 }
126
127 /// Format send-keys command for a specific window (if mapping exists)
128 fn format_send_keys_for_window(&self, data: &[u8]) -> Option<String> {
129 let active_tab_id = self.tab_manager.active_tab_id()?;
130
131 // Find the tmux window for this tab
132 let tmux_window_id = self.tmux_state.tmux_sync.get_window(active_tab_id)?;
133
134 // Format send-keys command with window target using proper escaping
135 let escaped = crate::tmux::escape_keys_for_tmux(data);
136 Some(format!("send-keys -t @{} {}\n", tmux_window_id, escaped))
137 }
138
139 /// Send input via tmux window target (fallback when no pane ID is set).
140 ///
141 /// This method is a planned fallback path for `TmuxSync` integration: when
142 /// the pane-level routing in `send_input_bytes` cannot resolve a pane ID,
143 /// routing via the tmux window target (`@N`) is the intended recovery.
144 /// It duplicates part of `format_send_keys_for_window` on purpose — the
145 /// caller needs to write directly rather than just format the command.
146 ///
147 /// Not yet wired up because `TmuxSync::get_window` is not yet called from
148 /// the hot-path input handler. Wire it up when implementing pane-less
149 /// gateway fallback (tracked as a GitHub issue under "tmux integration").
150 #[allow(dead_code)] // Infrastructure for TmuxSync pane-less fallback — not yet wired
151 fn send_input_via_tmux_window(&self, data: &[u8]) -> bool {
152 let active_tab_id = match self.tab_manager.active_tab_id() {
153 Some(id) => id,
154 None => return false,
155 };
156
157 // Find the tmux window for this tab
158 let tmux_window_id = match self.tmux_state.tmux_sync.get_window(active_tab_id) {
159 Some(id) => id,
160 None => {
161 crate::debug_trace!(
162 "TMUX",
163 "No tmux window mapping for tab {}, using untargeted send-keys",
164 active_tab_id
165 );
166 return false;
167 }
168 };
169
170 // Format send-keys command with window target using proper escaping
171 let escaped = crate::tmux::escape_keys_for_tmux(data);
172 let cmd = format!("send-keys -t @{} {}\n", tmux_window_id, escaped);
173
174 // Write to gateway tab
175 if self.write_to_gateway(&cmd) {
176 crate::debug_trace!(
177 "TMUX",
178 "Sent {} bytes via gateway to window @{}",
179 data.len(),
180 tmux_window_id
181 );
182 return true;
183 }
184
185 false
186 }
187
188 /// Send paste text through tmux gateway mode.
189 ///
190 /// Uses send-keys -l for literal text to handle special characters properly.
191 pub fn paste_via_tmux(&self, text: &str) -> bool {
192 if !self.config.load().tmux.tmux_enabled || !self.is_tmux_connected() {
193 return false;
194 }
195
196 let session = match &self.tmux_state.tmux_session {
197 Some(s) => s,
198 None => return false,
199 };
200
201 // Format the literal send command
202 let cmd = match session.format_send_literal(text) {
203 Some(c) => c,
204 None => return false,
205 };
206
207 // Write to gateway tab
208 if self.write_to_gateway(&cmd) {
209 crate::debug_info!("TMUX", "Pasted {} chars via gateway", text.len());
210 return true;
211 }
212
213 false
214 }
215
216 /// Handle tmux prefix key mode
217 ///
218 /// In control mode, we intercept the prefix key (e.g., Ctrl+B or Ctrl+Space)
219 /// and wait for the next key to translate into a tmux command.
220 ///
221 /// Returns true if the key was handled by the prefix system.
222 pub fn handle_tmux_prefix_key(&mut self, event: &winit::event::KeyEvent) -> bool {
223 // Only handle on key press
224 if event.state != winit::event::ElementState::Pressed {
225 return false;
226 }
227
228 // Only handle if tmux is connected
229 if !self.config.load().tmux.tmux_enabled || !self.is_tmux_connected() {
230 return false;
231 }
232
233 let modifiers = self.input_handler.modifiers.state();
234
235 // Check if we're in prefix mode (waiting for command key)
236 if self.tmux_state.tmux_prefix_state.is_active() {
237 // Ignore modifier-only key presses (Shift, Ctrl, Alt, Super)
238 // These are needed to type shifted characters like " and %
239 use winit::keyboard::{Key, NamedKey};
240 let is_modifier_only = matches!(
241 event.logical_key,
242 Key::Named(
243 NamedKey::Shift
244 | NamedKey::Control
245 | NamedKey::Alt
246 | NamedKey::Super
247 | NamedKey::Meta
248 )
249 );
250 if is_modifier_only {
251 crate::debug_trace!(
252 "TMUX",
253 "Ignoring modifier-only key in prefix mode: {:?}",
254 event.logical_key
255 );
256 return false; // Don't consume - let the modifier key through
257 }
258
259 // Exit prefix mode
260 self.tmux_state.tmux_prefix_state.exit();
261
262 // Get focused pane ID for targeted commands
263 let focused_pane = self
264 .tmux_state
265 .tmux_session
266 .as_ref()
267 .and_then(|s| s.focused_pane());
268
269 // Translate the command key to a tmux command
270 if let Some(cmd) =
271 crate::tmux::translate_command_key(&event.logical_key, modifiers, focused_pane)
272 {
273 crate::debug_info!(
274 "TMUX",
275 "Prefix command: {:?} -> {}",
276 event.logical_key,
277 cmd.trim()
278 );
279
280 // Send the command to tmux
281 if self.write_to_gateway(&cmd) {
282 // Show toast for certain commands (check command base, ignoring target)
283 let cmd_base = cmd.split(" -t").next().unwrap_or(&cmd).trim();
284 match cmd_base {
285 "detach-client" => self.show_toast("tmux: Detaching..."),
286 "new-window" => self.show_toast("tmux: New window"),
287 _ => {}
288 }
289 return true;
290 }
291 } else {
292 // Unknown command key - show feedback
293 crate::debug_info!(
294 "TMUX",
295 "Unknown prefix command key: {:?}",
296 event.logical_key
297 );
298 self.show_toast(format!(
299 "tmux: Unknown command key: {:?}",
300 event.logical_key
301 ));
302 }
303 return true; // Consumed the key even if unknown
304 }
305
306 // Check if this is the prefix key
307 if let Some(ref prefix_key) = self.tmux_state.tmux_prefix_key
308 && prefix_key.matches(&event.logical_key, modifiers)
309 {
310 crate::debug_info!("TMUX", "Prefix key pressed, entering prefix mode");
311 self.tmux_state.tmux_prefix_state.enter();
312 self.show_toast("tmux: prefix...");
313 return true;
314 }
315
316 false
317 }
318}