par-term-input 0.1.7

Input sequence generation for par-term terminal emulator
Documentation
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
//! Keyboard input handling and VT byte sequence generation for par-term.
//!
//! This crate converts `winit` keyboard events into the terminal input byte
//! sequences expected by shell applications. It handles character input,
//! named keys, function keys, modifier combinations, Option/Alt key modes,
//! clipboard operations, and the modifyOtherKeys protocol extension.
//!
//! The primary entry point is [`InputHandler`], which tracks modifier state
//! and translates each [`winit::event::KeyEvent`] into a `Vec<u8>` suitable
//! for writing directly to the PTY.

use arboard::Clipboard;
use winit::event::{ElementState, KeyEvent, Modifiers};
use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey};

use par_term_config::OptionKeyMode;

/// Input handler for converting winit events to terminal input
pub struct InputHandler {
    pub modifiers: Modifiers,
    clipboard: Option<Clipboard>,
    /// Option key mode for left Option/Alt key
    pub left_option_key_mode: OptionKeyMode,
    /// Option key mode for right Option/Alt key
    pub right_option_key_mode: OptionKeyMode,
    /// Track which Alt key is currently pressed (for determining mode on character input)
    /// True = left Alt is pressed, False = right Alt or no Alt
    left_alt_pressed: bool,
    /// True = right Alt is pressed
    right_alt_pressed: bool,
}

impl InputHandler {
    /// Create a new input handler
    pub fn new() -> Self {
        let clipboard = Clipboard::new().ok();
        if clipboard.is_none() {
            log::warn!("Failed to initialize clipboard support");
        }

        Self {
            modifiers: Modifiers::default(),
            clipboard,
            left_option_key_mode: OptionKeyMode::default(),
            right_option_key_mode: OptionKeyMode::default(),
            left_alt_pressed: false,
            right_alt_pressed: false,
        }
    }

    /// Update the current modifier state
    pub fn update_modifiers(&mut self, modifiers: Modifiers) {
        self.modifiers = modifiers;
    }

    /// Update Option/Alt key modes from config
    pub fn update_option_key_modes(&mut self, left: OptionKeyMode, right: OptionKeyMode) {
        self.left_option_key_mode = left;
        self.right_option_key_mode = right;
    }

    /// Track Alt key press/release to know which Alt is active
    pub fn track_alt_key(&mut self, event: &KeyEvent) {
        // Check if this is an Alt key event by physical key
        let is_left_alt = matches!(event.physical_key, PhysicalKey::Code(KeyCode::AltLeft));
        let is_right_alt = matches!(event.physical_key, PhysicalKey::Code(KeyCode::AltRight));

        if is_left_alt {
            self.left_alt_pressed = event.state == ElementState::Pressed;
        } else if is_right_alt {
            self.right_alt_pressed = event.state == ElementState::Pressed;
        }
    }

    /// Get the active Option key mode based on which Alt key is pressed
    fn get_active_option_mode(&self) -> OptionKeyMode {
        // If both are pressed, prefer left (arbitrary but consistent)
        // If only one is pressed, use that one's mode
        // If neither is pressed (shouldn't happen when alt modifier is set), default to left
        if self.left_alt_pressed {
            self.left_option_key_mode
        } else if self.right_alt_pressed {
            self.right_option_key_mode
        } else {
            // Fallback: both modes are the same in most configs, so use left
            self.left_option_key_mode
        }
    }

    /// Apply Option/Alt key transformation based on the configured mode
    fn apply_option_key_mode(&self, bytes: &mut Vec<u8>, original_char: char) {
        let mode = self.get_active_option_mode();

        match mode {
            OptionKeyMode::Normal => {
                // Normal mode: the character is already the special character from the OS
                // (e.g., Option+f = Æ’ on macOS). Don't modify it.
                // The bytes already contain the correct character from winit.
            }
            OptionKeyMode::Meta => {
                // Meta mode: set the high bit (8th bit) on the character
                // This only works for ASCII characters (0-127)
                if original_char.is_ascii() {
                    let meta_byte = (original_char as u8) | 0x80;
                    bytes.clear();
                    bytes.push(meta_byte);
                }
                // For non-ASCII, fall through to ESC mode behavior
                else {
                    bytes.insert(0, 0x1b);
                }
            }
            OptionKeyMode::Esc => {
                // Esc mode: send ESC prefix before the character
                // First, we need to use the base character, not the special character
                // This requires getting the unmodified key
                if original_char.is_ascii() {
                    bytes.clear();
                    bytes.push(0x1b); // ESC
                    bytes.push(original_char as u8);
                } else {
                    // For non-ASCII original characters, just prepend ESC to what we have
                    bytes.insert(0, 0x1b);
                }
            }
        }
    }

    /// Convert a keyboard event to terminal input bytes
    ///
    /// If `modify_other_keys_mode` is > 0, keys with modifiers will be reported
    /// using the XTerm modifyOtherKeys format: CSI 27 ; modifier ; keycode ~
    pub fn handle_key_event(&mut self, event: KeyEvent) -> Option<Vec<u8>> {
        self.handle_key_event_with_mode(event, 0, false)
    }

    /// Convert a keyboard event to terminal input bytes with modifyOtherKeys support
    ///
    /// `modify_other_keys_mode`:
    /// - 0: Disabled (normal key handling)
    /// - 1: Report modifiers for special keys only
    /// - 2: Report modifiers for all keys
    ///
    /// `application_cursor`: When true (DECCKM mode enabled), arrow keys send
    /// SS3 sequences (ESC O A) instead of CSI sequences (ESC [ A).
    pub fn handle_key_event_with_mode(
        &mut self,
        event: KeyEvent,
        modify_other_keys_mode: u8,
        application_cursor: bool,
    ) -> Option<Vec<u8>> {
        if event.state != ElementState::Pressed {
            return None;
        }

        let ctrl = self.modifiers.state().control_key();
        let alt = self.modifiers.state().alt_key();

        // Check if we should use modifyOtherKeys encoding
        if modify_other_keys_mode > 0
            && let Some(bytes) = self.try_modify_other_keys_encoding(&event, modify_other_keys_mode)
        {
            return Some(bytes);
        }

        match event.logical_key {
            // Character keys
            Key::Character(ref s) => {
                if ctrl {
                    // Handle Ctrl+key combinations
                    let ch = s.chars().next()?;

                    // Note: Ctrl+V paste is handled at higher level for bracketed paste support

                    if ch.is_ascii_alphabetic() {
                        // Ctrl+A through Ctrl+Z map to ASCII 1-26
                        let byte = (ch.to_ascii_lowercase() as u8) - b'a' + 1;
                        return Some(vec![byte]);
                    }
                }

                // Get the base character (without Alt modification) for Option key modes
                // We need to look at the physical key to get the unmodified character
                let base_char = self.get_base_character(&event);

                // Regular character input
                let mut bytes = s.as_bytes().to_vec();

                // Handle Alt/Option key based on configured mode
                if alt {
                    if let Some(base) = base_char {
                        self.apply_option_key_mode(&mut bytes, base);
                    } else {
                        // Fallback: if we can't determine base character, use the first char
                        let ch = s.chars().next().unwrap_or('\0');
                        self.apply_option_key_mode(&mut bytes, ch);
                    }
                }

                Some(bytes)
            }

            // Special keys
            Key::Named(named_key) => {
                // Handle Ctrl+Space specially - sends NUL (0x00)
                if ctrl && matches!(named_key, NamedKey::Space) {
                    return Some(vec![0x00]);
                }

                // Note: Shift+Insert paste is handled at higher level for bracketed paste support

                let shift = self.modifiers.state().shift_key();

                let seq = match named_key {
                    // Shift+Enter sends LF (newline) for soft line breaks (like iTerm2)
                    // Regular Enter sends CR (carriage return) for command execution
                    NamedKey::Enter => {
                        if shift {
                            "\n"
                        } else {
                            "\r"
                        }
                    }
                    // Shift+Tab sends reverse-tab escape sequence (CSI Z)
                    // Regular Tab sends HT (horizontal tab)
                    NamedKey::Tab => {
                        if shift {
                            "\x1b[Z"
                        } else {
                            "\t"
                        }
                    }
                    NamedKey::Space => " ",
                    NamedKey::Backspace => "\x7f",
                    NamedKey::Escape => "\x1b",
                    NamedKey::Insert => "\x1b[2~",
                    NamedKey::Delete => "\x1b[3~",

                    // Arrow keys - use SS3 (ESC O) in application cursor mode,
                    // CSI (ESC [) in normal mode
                    NamedKey::ArrowUp => {
                        if application_cursor {
                            "\x1bOA"
                        } else {
                            "\x1b[A"
                        }
                    }
                    NamedKey::ArrowDown => {
                        if application_cursor {
                            "\x1bOB"
                        } else {
                            "\x1b[B"
                        }
                    }
                    NamedKey::ArrowRight => {
                        if application_cursor {
                            "\x1bOC"
                        } else {
                            "\x1b[C"
                        }
                    }
                    NamedKey::ArrowLeft => {
                        if application_cursor {
                            "\x1bOD"
                        } else {
                            "\x1b[D"
                        }
                    }

                    // Navigation keys
                    NamedKey::Home => "\x1b[H",
                    NamedKey::End => "\x1b[F",
                    NamedKey::PageUp => "\x1b[5~",
                    NamedKey::PageDown => "\x1b[6~",

                    // Function keys
                    NamedKey::F1 => "\x1bOP",
                    NamedKey::F2 => "\x1bOQ",
                    NamedKey::F3 => "\x1bOR",
                    NamedKey::F4 => "\x1bOS",
                    NamedKey::F5 => "\x1b[15~",
                    NamedKey::F6 => "\x1b[17~",
                    NamedKey::F7 => "\x1b[18~",
                    NamedKey::F8 => "\x1b[19~",
                    NamedKey::F9 => "\x1b[20~",
                    NamedKey::F10 => "\x1b[21~",
                    NamedKey::F11 => "\x1b[23~",
                    NamedKey::F12 => "\x1b[24~",

                    _ => return None,
                };

                Some(seq.as_bytes().to_vec())
            }

            _ => None,
        }
    }

    /// Try to encode a key event using modifyOtherKeys format
    ///
    /// Returns Some(bytes) if the key should be encoded with modifyOtherKeys,
    /// None if normal handling should be used.
    ///
    /// modifyOtherKeys format: CSI 27 ; modifier ; keycode ~
    /// Where modifier is:
    /// - 2 = Shift
    /// - 3 = Alt
    /// - 4 = Shift+Alt
    /// - 5 = Ctrl
    /// - 6 = Shift+Ctrl
    /// - 7 = Alt+Ctrl
    /// - 8 = Shift+Alt+Ctrl
    fn try_modify_other_keys_encoding(&self, event: &KeyEvent, mode: u8) -> Option<Vec<u8>> {
        let ctrl = self.modifiers.state().control_key();
        let alt = self.modifiers.state().alt_key();
        let shift = self.modifiers.state().shift_key();

        // No modifiers means no special encoding needed
        if !ctrl && !alt && !shift {
            return None;
        }

        // Get the base character for the key
        let base_char = self.get_base_character(event)?;

        // Mode 1: Only report modifiers for keys that normally don't report them
        // Mode 2: Report modifiers for all keys
        if mode == 1 {
            // In mode 1, only use modifyOtherKeys for keys that would normally
            // lose modifier information (e.g., Ctrl+letter becomes control character)
            // Skip Shift-only since shifted letters are normally different characters
            if shift && !ctrl && !alt {
                return None;
            }
        }

        // Calculate the modifier value
        // bit 0 (1) = Shift
        // bit 1 (2) = Alt
        // bit 2 (4) = Ctrl
        // The final value is bits + 1
        let mut modifier_bits = 0u8;
        if shift {
            modifier_bits |= 1;
        }
        if alt {
            modifier_bits |= 2;
        }
        if ctrl {
            modifier_bits |= 4;
        }

        // Add 1 to get the XTerm modifier value (so no modifiers would be 1, but we already checked for that)
        let modifier_value = modifier_bits + 1;

        // Get the Unicode codepoint of the base character
        let keycode = base_char as u32;

        // Format: CSI 27 ; modifier ; keycode ~
        // CSI = ESC [
        Some(format!("\x1b[27;{};{}~", modifier_value, keycode).into_bytes())
    }

    /// Get the base character from a key event (the character without Alt modification)
    /// This maps physical key codes to their unmodified ASCII characters
    fn get_base_character(&self, event: &KeyEvent) -> Option<char> {
        // Map physical key codes to their base characters
        // This is needed because on macOS, Option+key produces a different logical character
        match event.physical_key {
            PhysicalKey::Code(code) => match code {
                KeyCode::KeyA => Some('a'),
                KeyCode::KeyB => Some('b'),
                KeyCode::KeyC => Some('c'),
                KeyCode::KeyD => Some('d'),
                KeyCode::KeyE => Some('e'),
                KeyCode::KeyF => Some('f'),
                KeyCode::KeyG => Some('g'),
                KeyCode::KeyH => Some('h'),
                KeyCode::KeyI => Some('i'),
                KeyCode::KeyJ => Some('j'),
                KeyCode::KeyK => Some('k'),
                KeyCode::KeyL => Some('l'),
                KeyCode::KeyM => Some('m'),
                KeyCode::KeyN => Some('n'),
                KeyCode::KeyO => Some('o'),
                KeyCode::KeyP => Some('p'),
                KeyCode::KeyQ => Some('q'),
                KeyCode::KeyR => Some('r'),
                KeyCode::KeyS => Some('s'),
                KeyCode::KeyT => Some('t'),
                KeyCode::KeyU => Some('u'),
                KeyCode::KeyV => Some('v'),
                KeyCode::KeyW => Some('w'),
                KeyCode::KeyX => Some('x'),
                KeyCode::KeyY => Some('y'),
                KeyCode::KeyZ => Some('z'),
                KeyCode::Digit0 => Some('0'),
                KeyCode::Digit1 => Some('1'),
                KeyCode::Digit2 => Some('2'),
                KeyCode::Digit3 => Some('3'),
                KeyCode::Digit4 => Some('4'),
                KeyCode::Digit5 => Some('5'),
                KeyCode::Digit6 => Some('6'),
                KeyCode::Digit7 => Some('7'),
                KeyCode::Digit8 => Some('8'),
                KeyCode::Digit9 => Some('9'),
                KeyCode::Minus => Some('-'),
                KeyCode::Equal => Some('='),
                KeyCode::BracketLeft => Some('['),
                KeyCode::BracketRight => Some(']'),
                KeyCode::Backslash => Some('\\'),
                KeyCode::Semicolon => Some(';'),
                KeyCode::Quote => Some('\''),
                KeyCode::Backquote => Some('`'),
                KeyCode::Comma => Some(','),
                KeyCode::Period => Some('.'),
                KeyCode::Slash => Some('/'),
                KeyCode::Space => Some(' '),
                _ => None,
            },
            _ => None,
        }
    }

    /// Paste text from clipboard (returns raw text, caller handles terminal conversion)
    pub fn paste_from_clipboard(&mut self) -> Option<String> {
        if let Some(ref mut clipboard) = self.clipboard {
            match clipboard.get_text() {
                Ok(text) => {
                    log::debug!("Pasting from clipboard: {} chars", text.len());
                    Some(text)
                }
                Err(e) => {
                    log::error!("Failed to get clipboard text: {}", e);
                    None
                }
            }
        } else {
            log::warn!("Clipboard not available");
            None
        }
    }

    /// Check if clipboard contains an image (used when text paste returns None
    /// to determine if we should forward the paste event to the terminal for
    /// image-aware applications like Claude Code)
    pub fn clipboard_has_image(&mut self) -> bool {
        if let Some(ref mut clipboard) = self.clipboard {
            let has_image = clipboard.get_image().is_ok();
            log::debug!("Clipboard image check: {}", has_image);
            has_image
        } else {
            false
        }
    }

    /// Copy text to clipboard
    pub fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String> {
        if let Some(ref mut clipboard) = self.clipboard {
            clipboard
                .set_text(text.to_string())
                .map_err(|e| format!("Failed to set clipboard text: {}", e))
        } else {
            Err("Clipboard not available".to_string())
        }
    }

    /// Copy text to primary selection (Linux X11 only)
    #[cfg(target_os = "linux")]
    pub fn copy_to_primary_selection(&mut self, text: &str) -> Result<(), String> {
        use arboard::SetExtLinux;

        if let Some(ref mut clipboard) = self.clipboard {
            clipboard
                .set()
                .clipboard(arboard::LinuxClipboardKind::Primary)
                .text(text.to_string())
                .map_err(|e| format!("Failed to set primary selection: {}", e))?;
            Ok(())
        } else {
            Err("Clipboard not available".to_string())
        }
    }

    /// Paste text from primary selection (Linux X11 only, returns raw text)
    #[cfg(target_os = "linux")]
    pub fn paste_from_primary_selection(&mut self) -> Option<String> {
        use arboard::GetExtLinux;

        if let Some(ref mut clipboard) = self.clipboard {
            match clipboard
                .get()
                .clipboard(arboard::LinuxClipboardKind::Primary)
                .text()
            {
                Ok(text) => {
                    log::debug!("Pasting from primary selection: {} chars", text.len());
                    Some(text)
                }
                Err(e) => {
                    log::error!("Failed to get primary selection text: {}", e);
                    None
                }
            }
        } else {
            log::warn!("Clipboard not available");
            None
        }
    }

    /// Fallback for non-Linux platforms - copy to primary selection not supported
    #[cfg(not(target_os = "linux"))]
    pub fn copy_to_primary_selection(&mut self, _text: &str) -> Result<(), String> {
        Ok(()) // No-op on non-Linux platforms
    }

    /// Fallback for non-Linux platforms - paste from primary selection uses regular clipboard
    #[cfg(not(target_os = "linux"))]
    pub fn paste_from_primary_selection(&mut self) -> Option<String> {
        self.paste_from_clipboard()
    }
}

impl Default for InputHandler {
    fn default() -> Self {
        Self::new()
    }
}