Skip to main content

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}