Skip to main content

par_term_input/
lib.rs

1//! Keyboard input handling and VT byte sequence generation for par-term.
2//!
3//! This crate converts `winit` keyboard events into the terminal input byte
4//! sequences expected by shell applications. It handles character input,
5//! named keys, function keys, modifier combinations, Option/Alt key modes,
6//! clipboard operations, and the modifyOtherKeys protocol extension.
7//!
8//! The primary entry point is [`InputHandler`], which tracks modifier state
9//! and translates each [`winit::event::KeyEvent`] into a `Vec<u8>` suitable
10//! for writing directly to the PTY.
11
12use arboard::Clipboard;
13use winit::event::{ElementState, KeyEvent, Modifiers};
14use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey};
15
16use par_term_config::OptionKeyMode;
17
18/// Input handler for converting winit events to terminal input
19pub struct InputHandler {
20    pub modifiers: Modifiers,
21    clipboard: Option<Clipboard>,
22    /// Option key mode for left Option/Alt key
23    pub left_option_key_mode: OptionKeyMode,
24    /// Option key mode for right Option/Alt key
25    pub right_option_key_mode: OptionKeyMode,
26    /// Track which Alt key is currently pressed (for determining mode on character input)
27    /// True = left Alt is pressed, False = right Alt or no Alt
28    left_alt_pressed: bool,
29    /// True = right Alt is pressed
30    right_alt_pressed: bool,
31}
32
33impl InputHandler {
34    /// Create a new input handler
35    pub fn new() -> Self {
36        let clipboard = Clipboard::new().ok();
37        if clipboard.is_none() {
38            log::warn!("Failed to initialize clipboard support");
39        }
40
41        Self {
42            modifiers: Modifiers::default(),
43            clipboard,
44            left_option_key_mode: OptionKeyMode::default(),
45            right_option_key_mode: OptionKeyMode::default(),
46            left_alt_pressed: false,
47            right_alt_pressed: false,
48        }
49    }
50
51    /// Update the current modifier state
52    pub fn update_modifiers(&mut self, modifiers: Modifiers) {
53        self.modifiers = modifiers;
54    }
55
56    /// Update Option/Alt key modes from config
57    pub fn update_option_key_modes(&mut self, left: OptionKeyMode, right: OptionKeyMode) {
58        self.left_option_key_mode = left;
59        self.right_option_key_mode = right;
60    }
61
62    /// Track Alt key press/release to know which Alt is active
63    pub fn track_alt_key(&mut self, event: &KeyEvent) {
64        // Check if this is an Alt key event by physical key
65        let is_left_alt = matches!(event.physical_key, PhysicalKey::Code(KeyCode::AltLeft));
66        let is_right_alt = matches!(event.physical_key, PhysicalKey::Code(KeyCode::AltRight));
67
68        if is_left_alt {
69            self.left_alt_pressed = event.state == ElementState::Pressed;
70        } else if is_right_alt {
71            self.right_alt_pressed = event.state == ElementState::Pressed;
72        }
73    }
74
75    /// Get the active Option key mode based on which Alt key is pressed
76    fn get_active_option_mode(&self) -> OptionKeyMode {
77        // If both are pressed, prefer left (arbitrary but consistent)
78        // If only one is pressed, use that one's mode
79        // If neither is pressed (shouldn't happen when alt modifier is set), default to left
80        if self.left_alt_pressed {
81            self.left_option_key_mode
82        } else if self.right_alt_pressed {
83            self.right_option_key_mode
84        } else {
85            // Fallback: both modes are the same in most configs, so use left
86            self.left_option_key_mode
87        }
88    }
89
90    /// Apply Option/Alt key transformation based on the configured mode
91    fn apply_option_key_mode(&self, bytes: &mut Vec<u8>, original_char: char) {
92        let mode = self.get_active_option_mode();
93
94        match mode {
95            OptionKeyMode::Normal => {
96                // Normal mode: the character is already the special character from the OS
97                // (e.g., Option+f = ƒ on macOS). Don't modify it.
98                // The bytes already contain the correct character from winit.
99            }
100            OptionKeyMode::Meta => {
101                // Meta mode: set the high bit (8th bit) on the character
102                // This only works for ASCII characters (0-127)
103                if original_char.is_ascii() {
104                    let meta_byte = (original_char as u8) | 0x80;
105                    bytes.clear();
106                    bytes.push(meta_byte);
107                }
108                // For non-ASCII, fall through to ESC mode behavior
109                else {
110                    bytes.insert(0, 0x1b);
111                }
112            }
113            OptionKeyMode::Esc => {
114                // Esc mode: send ESC prefix before the character
115                // First, we need to use the base character, not the special character
116                // This requires getting the unmodified key
117                if original_char.is_ascii() {
118                    bytes.clear();
119                    bytes.push(0x1b); // ESC
120                    bytes.push(original_char as u8);
121                } else {
122                    // For non-ASCII original characters, just prepend ESC to what we have
123                    bytes.insert(0, 0x1b);
124                }
125            }
126        }
127    }
128
129    /// Convert a keyboard event to terminal input bytes
130    ///
131    /// If `modify_other_keys_mode` is > 0, keys with modifiers will be reported
132    /// using the XTerm modifyOtherKeys format: CSI 27 ; modifier ; keycode ~
133    pub fn handle_key_event(&mut self, event: KeyEvent) -> Option<Vec<u8>> {
134        self.handle_key_event_with_mode(event, 0, false)
135    }
136
137    /// Convert a keyboard event to terminal input bytes with modifyOtherKeys support
138    ///
139    /// `modify_other_keys_mode`:
140    /// - 0: Disabled (normal key handling)
141    /// - 1: Report modifiers for special keys only
142    /// - 2: Report modifiers for all keys
143    ///
144    /// `application_cursor`: When true (DECCKM mode enabled), arrow keys send
145    /// SS3 sequences (ESC O A) instead of CSI sequences (ESC [ A).
146    pub fn handle_key_event_with_mode(
147        &mut self,
148        event: KeyEvent,
149        modify_other_keys_mode: u8,
150        application_cursor: bool,
151    ) -> Option<Vec<u8>> {
152        if event.state != ElementState::Pressed {
153            return None;
154        }
155
156        let ctrl = self.modifiers.state().control_key();
157        let alt = self.modifiers.state().alt_key();
158
159        // Check if we should use modifyOtherKeys encoding
160        if modify_other_keys_mode > 0
161            && let Some(bytes) = self.try_modify_other_keys_encoding(&event, modify_other_keys_mode)
162        {
163            return Some(bytes);
164        }
165
166        match event.logical_key {
167            // Character keys
168            Key::Character(ref s) => {
169                if ctrl {
170                    // Handle Ctrl+key combinations
171                    let ch = s.chars().next()?;
172
173                    // Note: Ctrl+V paste is handled at higher level for bracketed paste support
174
175                    if ch.is_ascii_alphabetic() {
176                        // Ctrl+A through Ctrl+Z map to ASCII 1-26
177                        let byte = (ch.to_ascii_lowercase() as u8) - b'a' + 1;
178                        return Some(vec![byte]);
179                    }
180                }
181
182                // Get the base character (without Alt modification) for Option key modes
183                // We need to look at the physical key to get the unmodified character
184                let base_char = self.get_base_character(&event);
185
186                // Regular character input
187                let mut bytes = s.as_bytes().to_vec();
188
189                // Handle Alt/Option key based on configured mode
190                if alt {
191                    if let Some(base) = base_char {
192                        self.apply_option_key_mode(&mut bytes, base);
193                    } else {
194                        // Fallback: if we can't determine base character, use the first char
195                        let ch = s.chars().next().unwrap_or('\0');
196                        self.apply_option_key_mode(&mut bytes, ch);
197                    }
198                }
199
200                Some(bytes)
201            }
202
203            // Special keys
204            Key::Named(named_key) => {
205                // Handle Ctrl+Space specially - sends NUL (0x00)
206                if ctrl && matches!(named_key, NamedKey::Space) {
207                    return Some(vec![0x00]);
208                }
209
210                // Note: Shift+Insert paste is handled at higher level for bracketed paste support
211
212                let shift = self.modifiers.state().shift_key();
213
214                let seq = match named_key {
215                    // Shift+Enter sends LF (newline) for soft line breaks (like iTerm2)
216                    // Regular Enter sends CR (carriage return) for command execution
217                    NamedKey::Enter => {
218                        if shift {
219                            "\n"
220                        } else {
221                            "\r"
222                        }
223                    }
224                    // Shift+Tab sends reverse-tab escape sequence (CSI Z)
225                    // Regular Tab sends HT (horizontal tab)
226                    NamedKey::Tab => {
227                        if shift {
228                            "\x1b[Z"
229                        } else {
230                            "\t"
231                        }
232                    }
233                    NamedKey::Space => " ",
234                    NamedKey::Backspace => "\x7f",
235                    NamedKey::Escape => "\x1b",
236                    NamedKey::Insert => "\x1b[2~",
237                    NamedKey::Delete => "\x1b[3~",
238
239                    // Arrow keys - use SS3 (ESC O) in application cursor mode,
240                    // CSI (ESC [) in normal mode
241                    NamedKey::ArrowUp => {
242                        if application_cursor {
243                            "\x1bOA"
244                        } else {
245                            "\x1b[A"
246                        }
247                    }
248                    NamedKey::ArrowDown => {
249                        if application_cursor {
250                            "\x1bOB"
251                        } else {
252                            "\x1b[B"
253                        }
254                    }
255                    NamedKey::ArrowRight => {
256                        if application_cursor {
257                            "\x1bOC"
258                        } else {
259                            "\x1b[C"
260                        }
261                    }
262                    NamedKey::ArrowLeft => {
263                        if application_cursor {
264                            "\x1bOD"
265                        } else {
266                            "\x1b[D"
267                        }
268                    }
269
270                    // Navigation keys
271                    NamedKey::Home => "\x1b[H",
272                    NamedKey::End => "\x1b[F",
273                    NamedKey::PageUp => "\x1b[5~",
274                    NamedKey::PageDown => "\x1b[6~",
275
276                    // Function keys
277                    NamedKey::F1 => "\x1bOP",
278                    NamedKey::F2 => "\x1bOQ",
279                    NamedKey::F3 => "\x1bOR",
280                    NamedKey::F4 => "\x1bOS",
281                    NamedKey::F5 => "\x1b[15~",
282                    NamedKey::F6 => "\x1b[17~",
283                    NamedKey::F7 => "\x1b[18~",
284                    NamedKey::F8 => "\x1b[19~",
285                    NamedKey::F9 => "\x1b[20~",
286                    NamedKey::F10 => "\x1b[21~",
287                    NamedKey::F11 => "\x1b[23~",
288                    NamedKey::F12 => "\x1b[24~",
289
290                    _ => return None,
291                };
292
293                Some(seq.as_bytes().to_vec())
294            }
295
296            _ => None,
297        }
298    }
299
300    /// Try to encode a key event using modifyOtherKeys format
301    ///
302    /// Returns Some(bytes) if the key should be encoded with modifyOtherKeys,
303    /// None if normal handling should be used.
304    ///
305    /// modifyOtherKeys format: CSI 27 ; modifier ; keycode ~
306    /// Where modifier is:
307    /// - 2 = Shift
308    /// - 3 = Alt
309    /// - 4 = Shift+Alt
310    /// - 5 = Ctrl
311    /// - 6 = Shift+Ctrl
312    /// - 7 = Alt+Ctrl
313    /// - 8 = Shift+Alt+Ctrl
314    fn try_modify_other_keys_encoding(&self, event: &KeyEvent, mode: u8) -> Option<Vec<u8>> {
315        let ctrl = self.modifiers.state().control_key();
316        let alt = self.modifiers.state().alt_key();
317        let shift = self.modifiers.state().shift_key();
318
319        // No modifiers means no special encoding needed
320        if !ctrl && !alt && !shift {
321            return None;
322        }
323
324        // Get the base character for the key
325        let base_char = self.get_base_character(event)?;
326
327        // Mode 1: Only report modifiers for keys that normally don't report them
328        // Mode 2: Report modifiers for all keys
329        if mode == 1 {
330            // In mode 1, only use modifyOtherKeys for keys that would normally
331            // lose modifier information (e.g., Ctrl+letter becomes control character)
332            // Skip Shift-only since shifted letters are normally different characters
333            if shift && !ctrl && !alt {
334                return None;
335            }
336        }
337
338        // Calculate the modifier value
339        // bit 0 (1) = Shift
340        // bit 1 (2) = Alt
341        // bit 2 (4) = Ctrl
342        // The final value is bits + 1
343        let mut modifier_bits = 0u8;
344        if shift {
345            modifier_bits |= 1;
346        }
347        if alt {
348            modifier_bits |= 2;
349        }
350        if ctrl {
351            modifier_bits |= 4;
352        }
353
354        // Add 1 to get the XTerm modifier value (so no modifiers would be 1, but we already checked for that)
355        let modifier_value = modifier_bits + 1;
356
357        // Get the Unicode codepoint of the base character
358        let keycode = base_char as u32;
359
360        // Format: CSI 27 ; modifier ; keycode ~
361        // CSI = ESC [
362        Some(format!("\x1b[27;{};{}~", modifier_value, keycode).into_bytes())
363    }
364
365    /// Get the base character from a key event (the character without Alt modification)
366    /// This maps physical key codes to their unmodified ASCII characters
367    fn get_base_character(&self, event: &KeyEvent) -> Option<char> {
368        // Map physical key codes to their base characters
369        // This is needed because on macOS, Option+key produces a different logical character
370        match event.physical_key {
371            PhysicalKey::Code(code) => match code {
372                KeyCode::KeyA => Some('a'),
373                KeyCode::KeyB => Some('b'),
374                KeyCode::KeyC => Some('c'),
375                KeyCode::KeyD => Some('d'),
376                KeyCode::KeyE => Some('e'),
377                KeyCode::KeyF => Some('f'),
378                KeyCode::KeyG => Some('g'),
379                KeyCode::KeyH => Some('h'),
380                KeyCode::KeyI => Some('i'),
381                KeyCode::KeyJ => Some('j'),
382                KeyCode::KeyK => Some('k'),
383                KeyCode::KeyL => Some('l'),
384                KeyCode::KeyM => Some('m'),
385                KeyCode::KeyN => Some('n'),
386                KeyCode::KeyO => Some('o'),
387                KeyCode::KeyP => Some('p'),
388                KeyCode::KeyQ => Some('q'),
389                KeyCode::KeyR => Some('r'),
390                KeyCode::KeyS => Some('s'),
391                KeyCode::KeyT => Some('t'),
392                KeyCode::KeyU => Some('u'),
393                KeyCode::KeyV => Some('v'),
394                KeyCode::KeyW => Some('w'),
395                KeyCode::KeyX => Some('x'),
396                KeyCode::KeyY => Some('y'),
397                KeyCode::KeyZ => Some('z'),
398                KeyCode::Digit0 => Some('0'),
399                KeyCode::Digit1 => Some('1'),
400                KeyCode::Digit2 => Some('2'),
401                KeyCode::Digit3 => Some('3'),
402                KeyCode::Digit4 => Some('4'),
403                KeyCode::Digit5 => Some('5'),
404                KeyCode::Digit6 => Some('6'),
405                KeyCode::Digit7 => Some('7'),
406                KeyCode::Digit8 => Some('8'),
407                KeyCode::Digit9 => Some('9'),
408                KeyCode::Minus => Some('-'),
409                KeyCode::Equal => Some('='),
410                KeyCode::BracketLeft => Some('['),
411                KeyCode::BracketRight => Some(']'),
412                KeyCode::Backslash => Some('\\'),
413                KeyCode::Semicolon => Some(';'),
414                KeyCode::Quote => Some('\''),
415                KeyCode::Backquote => Some('`'),
416                KeyCode::Comma => Some(','),
417                KeyCode::Period => Some('.'),
418                KeyCode::Slash => Some('/'),
419                KeyCode::Space => Some(' '),
420                _ => None,
421            },
422            _ => None,
423        }
424    }
425
426    /// Paste text from clipboard (returns raw text, caller handles terminal conversion)
427    pub fn paste_from_clipboard(&mut self) -> Option<String> {
428        if let Some(ref mut clipboard) = self.clipboard {
429            match clipboard.get_text() {
430                Ok(text) => {
431                    log::debug!("Pasting from clipboard: {} chars", text.len());
432                    Some(text)
433                }
434                Err(e) => {
435                    log::error!("Failed to get clipboard text: {}", e);
436                    None
437                }
438            }
439        } else {
440            log::warn!("Clipboard not available");
441            None
442        }
443    }
444
445    /// Check if clipboard contains an image (used when text paste returns None
446    /// to determine if we should forward the paste event to the terminal for
447    /// image-aware applications like Claude Code)
448    pub fn clipboard_has_image(&mut self) -> bool {
449        if let Some(ref mut clipboard) = self.clipboard {
450            let has_image = clipboard.get_image().is_ok();
451            log::debug!("Clipboard image check: {}", has_image);
452            has_image
453        } else {
454            false
455        }
456    }
457
458    /// Copy text to clipboard
459    pub fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String> {
460        if let Some(ref mut clipboard) = self.clipboard {
461            clipboard
462                .set_text(text.to_string())
463                .map_err(|e| format!("Failed to set clipboard text: {}", e))
464        } else {
465            Err("Clipboard not available".to_string())
466        }
467    }
468
469    /// Copy text to primary selection (Linux X11 only)
470    #[cfg(target_os = "linux")]
471    pub fn copy_to_primary_selection(&mut self, text: &str) -> Result<(), String> {
472        use arboard::SetExtLinux;
473
474        if let Some(ref mut clipboard) = self.clipboard {
475            clipboard
476                .set()
477                .clipboard(arboard::LinuxClipboardKind::Primary)
478                .text(text.to_string())
479                .map_err(|e| format!("Failed to set primary selection: {}", e))?;
480            Ok(())
481        } else {
482            Err("Clipboard not available".to_string())
483        }
484    }
485
486    /// Paste text from primary selection (Linux X11 only, returns raw text)
487    #[cfg(target_os = "linux")]
488    pub fn paste_from_primary_selection(&mut self) -> Option<String> {
489        use arboard::GetExtLinux;
490
491        if let Some(ref mut clipboard) = self.clipboard {
492            match clipboard
493                .get()
494                .clipboard(arboard::LinuxClipboardKind::Primary)
495                .text()
496            {
497                Ok(text) => {
498                    log::debug!("Pasting from primary selection: {} chars", text.len());
499                    Some(text)
500                }
501                Err(e) => {
502                    log::error!("Failed to get primary selection text: {}", e);
503                    None
504                }
505            }
506        } else {
507            log::warn!("Clipboard not available");
508            None
509        }
510    }
511
512    /// Fallback for non-Linux platforms - copy to primary selection not supported
513    #[cfg(not(target_os = "linux"))]
514    pub fn copy_to_primary_selection(&mut self, _text: &str) -> Result<(), String> {
515        Ok(()) // No-op on non-Linux platforms
516    }
517
518    /// Fallback for non-Linux platforms - paste from primary selection uses regular clipboard
519    #[cfg(not(target_os = "linux"))]
520    pub fn paste_from_primary_selection(&mut self) -> Option<String> {
521        self.paste_from_clipboard()
522    }
523}
524
525impl Default for InputHandler {
526    fn default() -> Self {
527        Self::new()
528    }
529}