diff --git a/src/pty/mod.rs b/src/pty/mod.rs index 0ad7ee73..f13080f9 100644 --- a/src/pty/mod.rs +++ b/src/pty/mod.rs @@ -91,8 +91,8 @@ pub enum PtyState { #[derive(Debug)] pub enum PtyMessage { /// Data from local terminal to send to remote - /// SmallVec<[u8; 8]> keeps key sequences stack-allocated - LocalInput(SmallVec<[u8; 8]>), + /// SmallVec<[u8; 64]> handles both key sequences and paste content without allocation + LocalInput(SmallVec<[u8; 64]>), /// Data from remote to display on local terminal /// SmallVec<[u8; 64]> handles most terminal output without allocation RemoteOutput(SmallVec<[u8; 64]>), diff --git a/src/pty/session/input.rs b/src/pty/session/input.rs index 640071e3..be77d54c 100644 --- a/src/pty/session/input.rs +++ b/src/pty/session/input.rs @@ -20,7 +20,7 @@ use smallvec::SmallVec; /// Handle input events and convert them to raw bytes /// Returns SmallVec to avoid heap allocations for small key sequences -pub fn handle_input_event(event: Event) -> Option> { +pub fn handle_input_event(event: Event) -> Option> { match event { Event::Key(key_event) => { // Only process key press events (not release) @@ -34,6 +34,23 @@ pub fn handle_input_event(event: Event) -> Option> { // TODO: Implement mouse event handling mouse_event_to_bytes(mouse_event) } + Event::Paste(text) => { + // Handle paste events from bracketed paste mode + // Return None for empty paste to avoid unnecessary channel sends + if text.is_empty() { + return None; + } + // Limit paste size to 1MB to prevent memory exhaustion attacks + const MAX_PASTE_SIZE: usize = 1024 * 1024; // 1MB + let bytes = text.into_bytes(); + if bytes.len() > MAX_PASTE_SIZE { + // Truncate to max size - this is a safety limit, not expected in normal use + Some(SmallVec::from_vec(bytes[..MAX_PASTE_SIZE].to_vec())) + } else { + // Use from_vec to avoid double memory copy + Some(SmallVec::from_vec(bytes)) + } + } Event::Resize(_width, _height) => { // Resize events are handled separately // This shouldn't happen as we handle resize via signals @@ -45,7 +62,7 @@ pub fn handle_input_event(event: Event) -> Option> { /// Convert key events to raw byte sequences /// Uses SmallVec to avoid heap allocations for key sequences (typically 1-5 bytes) -pub fn key_event_to_bytes(key_event: KeyEvent) -> Option> { +pub fn key_event_to_bytes(key_event: KeyEvent) -> Option> { match key_event { // Handle special key combinations KeyEvent { @@ -186,8 +203,881 @@ pub fn key_event_to_bytes(key_event: KeyEvent) -> Option> { } /// Convert mouse events to raw byte sequences -pub fn mouse_event_to_bytes(_mouse_event: MouseEvent) -> Option> { +pub fn mouse_event_to_bytes(_mouse_event: MouseEvent) -> Option> { // TODO: Implement mouse event to bytes conversion // This requires implementing the terminal mouse reporting protocol None } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_paste_event_small() { + // Test paste event with small text (< 64 bytes) + let text = "Hello, World!".to_string(); + let event = Event::Paste(text.clone()); + let result = handle_input_event(event); + + assert!(result.is_some()); + let bytes = result.unwrap(); + assert_eq!(bytes.as_slice(), text.as_bytes()); + } + + #[test] + fn test_paste_event_large() { + // Test paste event with large text (> 64 bytes) + let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. \ + Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \ + Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris." + .to_string(); + let event = Event::Paste(text.clone()); + let result = handle_input_event(event); + + assert!(result.is_some()); + let bytes = result.unwrap(); + assert_eq!(bytes.as_slice(), text.as_bytes()); + assert!( + bytes.len() > 64, + "Test data should be larger than SmallVec inline capacity" + ); + } + + #[test] + fn test_paste_event_empty() { + // Test paste event with empty text returns None + // Empty paste should not send unnecessary channel messages + let text = String::new(); + let event = Event::Paste(text); + let result = handle_input_event(event); + + assert!(result.is_none(), "Empty paste should return None"); + } + + #[test] + fn test_paste_event_special_chars() { + // Test paste event with special characters and newlines + let text = "Line 1\nLine 2\nLine 3\n\tTabbed\r\nCRLF".to_string(); + let event = Event::Paste(text.clone()); + let result = handle_input_event(event); + + assert!(result.is_some()); + let bytes = result.unwrap(); + assert_eq!(bytes.as_slice(), text.as_bytes()); + } + + #[test] + fn test_paste_event_unicode() { + // Test paste event with Unicode characters + let text = "Hello δΈ–η•Œ 🌍 Ω…Ψ±Ψ­Ψ¨Ψ§".to_string(); + let event = Event::Paste(text.clone()); + let result = handle_input_event(event); + + assert!(result.is_some()); + let bytes = result.unwrap(); + assert_eq!(bytes.as_slice(), text.as_bytes()); + } + + #[test] + fn test_paste_event_multiline() { + // Test paste event with multi-line content + let text = "#!/bin/bash\n\ + echo 'Hello, World!'\n\ + for i in {1..5}; do\n\ + \techo \"Number: $i\"\n\ + done" + .to_string(); + let event = Event::Paste(text.clone()); + let result = handle_input_event(event); + + assert!(result.is_some()); + let bytes = result.unwrap(); + assert_eq!(bytes.as_slice(), text.as_bytes()); + } + + #[test] + fn test_key_event_still_works() { + // Ensure regular key events still work after adding paste support + let event = Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + let bytes = result.unwrap(); + assert_eq!(bytes.as_slice(), b"a"); + } + + #[test] + fn test_resize_event_ignored() { + // Ensure resize events are still ignored + let event = Event::Resize(80, 24); + let result = handle_input_event(event); + + assert!(result.is_none()); + } + + #[test] + fn test_paste_event_size_limit() { + // Test that paste is truncated to MAX_PASTE_SIZE (1MB) + const MAX_PASTE_SIZE: usize = 1024 * 1024; + // Create a string larger than the limit + let text = "A".repeat(MAX_PASTE_SIZE + 1000); + let event = Event::Paste(text); + let result = handle_input_event(event); + + assert!(result.is_some()); + let bytes = result.unwrap(); + assert_eq!( + bytes.len(), + MAX_PASTE_SIZE, + "Paste should be truncated to MAX_PASTE_SIZE" + ); + } + + // ============================================ + // Ctrl+key combination tests + // ============================================ + + #[test] + fn test_ctrl_c_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('c'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_C_SEQUENCE); + } + + #[test] + fn test_ctrl_c_uppercase() { + // Ctrl+C should work with uppercase C as well + let event = Event::Key(KeyEvent { + code: KeyCode::Char('C'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_C_SEQUENCE); + } + + #[test] + fn test_ctrl_d_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('d'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_D_SEQUENCE); + } + + #[test] + fn test_ctrl_z_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('z'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_Z_SEQUENCE); + } + + #[test] + fn test_ctrl_a_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_A_SEQUENCE); + } + + #[test] + fn test_ctrl_e_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('e'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_E_SEQUENCE); + } + + #[test] + fn test_ctrl_u_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('u'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_U_SEQUENCE); + } + + #[test] + fn test_ctrl_k_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_K_SEQUENCE); + } + + #[test] + fn test_ctrl_w_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('w'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_W_SEQUENCE); + } + + #[test] + fn test_ctrl_l_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('l'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_L_SEQUENCE); + } + + #[test] + fn test_ctrl_r_sequence() { + let event = Event::Key(KeyEvent { + code: KeyCode::Char('r'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), CTRL_R_SEQUENCE); + } + + #[test] + fn test_ctrl_b_general_handler() { + // Ctrl+B should be handled by general Ctrl+ handler + let event = Event::Key(KeyEvent { + code: KeyCode::Char('b'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + // Ctrl+B = 0x02 + assert_eq!(result.unwrap().as_slice(), &[0x02]); + } + + #[test] + fn test_ctrl_f_general_handler() { + // Ctrl+F should be handled by general Ctrl+ handler + let event = Event::Key(KeyEvent { + code: KeyCode::Char('f'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + // Ctrl+F = 0x06 + assert_eq!(result.unwrap().as_slice(), &[0x06]); + } + + // ============================================ + // Arrow key tests + // ============================================ + + #[test] + fn test_up_arrow() { + let event = Event::Key(KeyEvent { + code: KeyCode::Up, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), UP_ARROW_SEQUENCE); + } + + #[test] + fn test_down_arrow() { + let event = Event::Key(KeyEvent { + code: KeyCode::Down, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), DOWN_ARROW_SEQUENCE); + } + + #[test] + fn test_left_arrow() { + let event = Event::Key(KeyEvent { + code: KeyCode::Left, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), LEFT_ARROW_SEQUENCE); + } + + #[test] + fn test_right_arrow() { + let event = Event::Key(KeyEvent { + code: KeyCode::Right, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), RIGHT_ARROW_SEQUENCE); + } + + // ============================================ + // Function key tests (F1-F12) + // ============================================ + + #[test] + fn test_f1_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(1), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F1_SEQUENCE); + } + + #[test] + fn test_f2_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(2), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F2_SEQUENCE); + } + + #[test] + fn test_f3_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(3), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F3_SEQUENCE); + } + + #[test] + fn test_f4_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(4), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F4_SEQUENCE); + } + + #[test] + fn test_f5_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(5), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F5_SEQUENCE); + } + + #[test] + fn test_f6_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(6), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F6_SEQUENCE); + } + + #[test] + fn test_f7_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(7), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F7_SEQUENCE); + } + + #[test] + fn test_f8_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(8), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F8_SEQUENCE); + } + + #[test] + fn test_f9_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(9), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F9_SEQUENCE); + } + + #[test] + fn test_f10_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(10), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F10_SEQUENCE); + } + + #[test] + fn test_f11_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(11), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F11_SEQUENCE); + } + + #[test] + fn test_f12_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::F(12), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), F12_SEQUENCE); + } + + #[test] + fn test_f13_key_not_supported() { + // F13+ should not be supported + let event = Event::Key(KeyEvent { + code: KeyCode::F(13), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_none()); + } + + // ============================================ + // Special key tests + // ============================================ + + #[test] + fn test_enter_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::Enter, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), ENTER_SEQUENCE); + } + + #[test] + fn test_tab_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::Tab, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), TAB_SEQUENCE); + } + + #[test] + fn test_backspace_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::Backspace, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), BACKSPACE_SEQUENCE); + } + + #[test] + fn test_escape_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::Esc, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), ESC_SEQUENCE); + } + + #[test] + fn test_home_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::Home, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), HOME_SEQUENCE); + } + + #[test] + fn test_end_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::End, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), END_SEQUENCE); + } + + #[test] + fn test_page_up_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::PageUp, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), PAGE_UP_SEQUENCE); + } + + #[test] + fn test_page_down_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::PageDown, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), PAGE_DOWN_SEQUENCE); + } + + #[test] + fn test_insert_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::Insert, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), INSERT_SEQUENCE); + } + + #[test] + fn test_delete_key() { + let event = Event::Key(KeyEvent { + code: KeyCode::Delete, + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), DELETE_SEQUENCE); + } + + // ============================================ + // KeyEventKind tests + // ============================================ + + #[test] + fn test_key_release_ignored() { + // Key release events should return None + let event = Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Release, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_none(), "Key release events should be ignored"); + } + + #[test] + fn test_key_repeat_ignored() { + // Key repeat events should also return None (only Press is processed) + let event = Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Repeat, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_none(), "Key repeat events should be ignored"); + } + + // ============================================ + // Modifier key tests + // ============================================ + + #[test] + fn test_shift_character() { + // Shift+a should produce 'A' (handled by crossterm, we get uppercase char) + // But we test that SHIFT modifier alone is accepted + let event = Event::Key(KeyEvent { + code: KeyCode::Char('A'), + modifiers: KeyModifiers::SHIFT, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), b"A"); + } + + #[test] + fn test_alt_character_ignored() { + // Alt+character should be ignored by our char handler + // (special handling would be needed for Alt sequences) + let event = Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::ALT, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!( + result.is_none(), + "Alt+char should be ignored (no special handler)" + ); + } + + #[test] + fn test_meta_character_ignored() { + // Meta+character should be ignored + let event = Event::Key(KeyEvent { + code: KeyCode::Char('a'), + modifiers: KeyModifiers::META, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!( + result.is_none(), + "Meta+char should be ignored (no special handler)" + ); + } + + #[test] + fn test_unicode_character() { + // Test Unicode character input + let event = Event::Key(KeyEvent { + code: KeyCode::Char('ν•œ'), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), "ν•œ".as_bytes()); + } + + #[test] + fn test_emoji_character() { + // Test emoji input + let event = Event::Key(KeyEvent { + code: KeyCode::Char('πŸš€'), + modifiers: KeyModifiers::NONE, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_some()); + assert_eq!(result.unwrap().as_slice(), "πŸš€".as_bytes()); + } + + // ============================================ + // Mouse event tests + // ============================================ + + #[test] + fn test_mouse_event_returns_none() { + // Mouse events are not currently supported + let event = Event::Mouse(MouseEvent { + kind: crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left), + column: 0, + row: 0, + modifiers: KeyModifiers::NONE, + }); + let result = handle_input_event(event); + + assert!(result.is_none(), "Mouse events should return None"); + } + + // ============================================ + // key_event_to_bytes direct tests + // ============================================ + + #[test] + fn test_key_event_to_bytes_ctrl_out_of_range() { + // Test that Ctrl+non-letter returns None (e.g., Ctrl+1) + // Since KeyCode::Char only accepts chars, test with a char outside a-z + // Actually, the general handler computes (c.to_ascii_lowercase() as u8).saturating_sub(b'a' - 1) + // For '1' = 49, to_ascii_lowercase() = 49, subtract 96 = max(0, -47) = 0 via saturating_sub + // But 0 <= 26 is false (0 < 1), so... let's trace through: + // '1' as u8 = 49, b'a' - 1 = 96, 49.saturating_sub(96) = 0 + // 0 <= 26 is true, so it would return Some([0]) + // Actually we need to test with a character that results in > 26 + // Let's test with '{' = 123, 123 - 96 = 27, which is > 26 + let key_event = KeyEvent { + code: KeyCode::Char('{'), + modifiers: KeyModifiers::CONTROL, + kind: KeyEventKind::Press, + state: crossterm::event::KeyEventState::NONE, + }; + let result = key_event_to_bytes(key_event); + + assert!(result.is_none()); + } +} diff --git a/src/pty/terminal.rs b/src/pty/terminal.rs index 7a86fff7..ba13f6a5 100644 --- a/src/pty/terminal.rs +++ b/src/pty/terminal.rs @@ -15,7 +15,11 @@ //! Terminal state management for PTY sessions. use anyhow::{Context, Result}; -use crossterm::terminal::{disable_raw_mode, enable_raw_mode}; +use crossterm::{ + event::{DisableBracketedPaste, EnableBracketedPaste}, + execute, + terminal::{disable_raw_mode, enable_raw_mode}, +}; use once_cell::sync::Lazy; use std::sync::{ atomic::{AtomicBool, Ordering}, @@ -76,6 +80,10 @@ impl TerminalStateGuard { is_raw_mode_active.store(true, Ordering::Relaxed); } + // Enable bracketed paste mode + execute!(std::io::stdout(), EnableBracketedPaste) + .with_context(|| "Failed to enable bracketed paste mode")?; + Ok(Self { saved_state, is_raw_mode_active, @@ -152,6 +160,11 @@ impl TerminalStateGuard { // Use global synchronization to prevent race conditions let _guard = TERMINAL_MUTEX.lock().unwrap(); + // Disable bracketed paste mode + if let Err(e) = execute!(std::io::stdout(), DisableBracketedPaste) { + eprintln!("Warning: Failed to disable bracketed paste mode during cleanup: {e}"); + } + // Exit raw mode if it's globally active if RAW_MODE_ACTIVE.load(Ordering::SeqCst) { if let Err(e) = disable_raw_mode() {