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}