Skip to main content

par_term_input/
lib.rs

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