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}