From a37bd82aea186e0e447ab5e9b46013dc7ec938cf Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Tue, 16 Dec 2025 13:42:23 +0900 Subject: [PATCH 1/4] fix: Enable paste operations in PTY mode sessions Resolves clipboard paste issues in PTY SSH sessions by implementing bracketed paste mode support and handling Event::Paste events. Changes: - Enable bracketed paste mode in TerminalStateGuard::new() - Disable bracketed paste mode in restore_terminal_state() - Add Event::Paste handler in handle_input_event() - Increase SmallVec capacity from 8 to 64 bytes for paste content - Update PtyMessage::LocalInput to use SmallVec<[u8; 64]> - Add comprehensive unit tests for paste event handling This fixes paste operations on all platforms (macOS Cmd+V, Linux Ctrl+Shift+V, Windows Ctrl+V) by properly capturing and forwarding pasted content through the SSH channel. Tests: All 455 tests pass, including 8 new paste event tests --- src/pty/mod.rs | 4 +- src/pty/session/input.rs | 122 ++++++++++++++++++++++++++++++++++++++- src/pty/terminal.rs | 15 ++++- 3 files changed, 135 insertions(+), 6 deletions(-) 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..ec16a107 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,11 @@ 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 + let bytes = text.into_bytes(); + Some(SmallVec::from_slice(&bytes)) + } Event::Resize(_width, _height) => { // Resize events are handled separately // This shouldn't happen as we handle resize via signals @@ -45,7 +50,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 +191,119 @@ 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 + let text = String::new(); + 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(), &[]); + } + + #[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()); + } +} 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() { From 077358712554f80fcdb7dfecaeae25903edb020d Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Tue, 16 Dec 2025 13:48:39 +0900 Subject: [PATCH 2/4] fix: Add paste size limit and optimize memory handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 1MB size limit to prevent memory exhaustion attacks (H1 issue) - Return None for empty paste to avoid unnecessary channel sends (M1 issue) - Use from_vec() instead of into_bytes() + from_slice() to avoid double copy (L1 issue) - Add test for paste size limit behavior πŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/pty/session/input.rs | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/src/pty/session/input.rs b/src/pty/session/input.rs index ec16a107..6547f242 100644 --- a/src/pty/session/input.rs +++ b/src/pty/session/input.rs @@ -36,8 +36,20 @@ pub fn handle_input_event(event: Event) -> Option> { } 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(); - Some(SmallVec::from_slice(&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 @@ -231,14 +243,13 @@ mod tests { #[test] fn test_paste_event_empty() { - // Test paste event with empty text + // 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.clone()); + let event = Event::Paste(text); let result = handle_input_event(event); - assert!(result.is_some()); - let bytes = result.unwrap(); - assert_eq!(bytes.as_slice(), &[]); + assert!(result.is_none(), "Empty paste should return None"); } #[test] @@ -306,4 +317,18 @@ mod tests { 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"); + } } From c73103ea0c7da06a171ba515faa4ec014ccb5268 Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Tue, 16 Dec 2025 14:06:10 +0900 Subject: [PATCH 3/4] style: Apply cargo fmt formatting to test assertions --- src/pty/session/input.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pty/session/input.rs b/src/pty/session/input.rs index 6547f242..b9f258d4 100644 --- a/src/pty/session/input.rs +++ b/src/pty/session/input.rs @@ -238,7 +238,10 @@ mod tests { 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"); + assert!( + bytes.len() > 64, + "Test data should be larger than SmallVec inline capacity" + ); } #[test] @@ -329,6 +332,10 @@ mod tests { assert!(result.is_some()); let bytes = result.unwrap(); - assert_eq!(bytes.len(), MAX_PASTE_SIZE, "Paste should be truncated to MAX_PASTE_SIZE"); + assert_eq!( + bytes.len(), + MAX_PASTE_SIZE, + "Paste should be truncated to MAX_PASTE_SIZE" + ); } } From befa8deb09e02689a759c4ef06a477344390c42a Mon Sep 17 00:00:00 2001 From: Jeongkyu Shin Date: Tue, 16 Dec 2025 14:16:36 +0900 Subject: [PATCH 4/4] test: Add comprehensive tests for key input handling - Add tests for all Ctrl+key combinations (C, D, Z, A, E, U, K, W, L, R, B, F) - Add tests for arrow keys (Up, Down, Left, Right) - Add tests for all function keys (F1-F12) and verify F13+ returns None - Add tests for special keys (Enter, Tab, Backspace, Esc, Home, End, PageUp, PageDown, Insert, Delete) - Add tests for KeyEventKind::Release and Repeat being ignored - Add tests for modifier keys (Shift accepted, Alt/Meta ignored for chars) - Add tests for Unicode and emoji character input - Add test for mouse events returning None - Add test for Ctrl+char out of range --- src/pty/session/input.rs | 742 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 742 insertions(+) diff --git a/src/pty/session/input.rs b/src/pty/session/input.rs index b9f258d4..be77d54c 100644 --- a/src/pty/session/input.rs +++ b/src/pty/session/input.rs @@ -338,4 +338,746 @@ mod tests { "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()); + } }