par_term/
input.rs

1use arboard::Clipboard;
2use winit::event::{ElementState, KeyEvent, Modifiers};
3use winit::keyboard::{Key, NamedKey};
4
5/// Input handler for converting winit events to terminal input
6pub struct InputHandler {
7    pub modifiers: Modifiers,
8    clipboard: Option<Clipboard>,
9}
10
11impl InputHandler {
12    /// Create a new input handler
13    pub fn new() -> Self {
14        let clipboard = Clipboard::new().ok();
15        if clipboard.is_none() {
16            log::warn!("Failed to initialize clipboard support");
17        }
18
19        Self {
20            modifiers: Modifiers::default(),
21            clipboard,
22        }
23    }
24
25    /// Update the current modifier state
26    pub fn update_modifiers(&mut self, modifiers: Modifiers) {
27        self.modifiers = modifiers;
28    }
29
30    /// Convert a keyboard event to terminal input bytes
31    pub fn handle_key_event(&mut self, event: KeyEvent) -> Option<Vec<u8>> {
32        if event.state != ElementState::Pressed {
33            return None;
34        }
35
36        let ctrl = self.modifiers.state().control_key();
37        let shift = self.modifiers.state().shift_key();
38        let alt = self.modifiers.state().alt_key();
39
40        match event.logical_key {
41            // Character keys
42            Key::Character(ref s) => {
43                if ctrl {
44                    // Handle Ctrl+key combinations
45                    let ch = s.chars().next()?;
46
47                    // Special case: Ctrl+V for paste
48                    if ch.eq_ignore_ascii_case(&'v') {
49                        return self.paste_from_clipboard();
50                    }
51
52                    if ch.is_ascii_alphabetic() {
53                        // Ctrl+A through Ctrl+Z map to ASCII 1-26
54                        let byte = (ch.to_ascii_lowercase() as u8) - b'a' + 1;
55                        return Some(vec![byte]);
56                    }
57                }
58
59                // Regular character input
60                let mut bytes = s.as_bytes().to_vec();
61
62                // Handle Alt key (sends ESC prefix)
63                if alt {
64                    bytes.insert(0, 0x1b);
65                }
66
67                Some(bytes)
68            }
69
70            // Special keys
71            Key::Named(named_key) => {
72                // Handle Ctrl+Space specially - sends NUL (0x00)
73                if ctrl && matches!(named_key, NamedKey::Space) {
74                    return Some(vec![0x00]);
75                }
76
77                // Handle Shift+Insert for paste
78                if shift && matches!(named_key, NamedKey::Insert) {
79                    return self.paste_from_clipboard();
80                }
81
82                let seq = match named_key {
83                    NamedKey::Enter => "\r",
84                    NamedKey::Tab => "\t",
85                    NamedKey::Space => " ",
86                    NamedKey::Backspace => "\x7f",
87                    NamedKey::Escape => "\x1b",
88                    NamedKey::Insert => "\x1b[2~",
89                    NamedKey::Delete => "\x1b[3~",
90
91                    // Arrow keys
92                    NamedKey::ArrowUp => "\x1b[A",
93                    NamedKey::ArrowDown => "\x1b[B",
94                    NamedKey::ArrowRight => "\x1b[C",
95                    NamedKey::ArrowLeft => "\x1b[D",
96
97                    // Navigation keys
98                    NamedKey::Home => "\x1b[H",
99                    NamedKey::End => "\x1b[F",
100                    NamedKey::PageUp => "\x1b[5~",
101                    NamedKey::PageDown => "\x1b[6~",
102
103                    // Function keys
104                    NamedKey::F1 => "\x1bOP",
105                    NamedKey::F2 => "\x1bOQ",
106                    NamedKey::F3 => "\x1bOR",
107                    NamedKey::F4 => "\x1bOS",
108                    NamedKey::F5 => "\x1b[15~",
109                    NamedKey::F6 => "\x1b[17~",
110                    NamedKey::F7 => "\x1b[18~",
111                    NamedKey::F8 => "\x1b[19~",
112                    NamedKey::F9 => "\x1b[20~",
113                    NamedKey::F10 => "\x1b[21~",
114                    NamedKey::F11 => "\x1b[23~",
115                    NamedKey::F12 => "\x1b[24~",
116
117                    _ => return None,
118                };
119
120                Some(seq.as_bytes().to_vec())
121            }
122
123            _ => None,
124        }
125    }
126
127    /// Paste text from clipboard
128    pub fn paste_from_clipboard(&mut self) -> Option<Vec<u8>> {
129        if let Some(ref mut clipboard) = self.clipboard {
130            match clipboard.get_text() {
131                Ok(text) => {
132                    log::debug!("Pasting from clipboard: {} chars", text.len());
133                    // Convert newlines to carriage returns for terminal
134                    let text = text.replace('\n', "\r");
135                    Some(text.as_bytes().to_vec())
136                }
137                Err(e) => {
138                    log::error!("Failed to get clipboard text: {}", e);
139                    None
140                }
141            }
142        } else {
143            log::warn!("Clipboard not available");
144            None
145        }
146    }
147
148    /// Copy text to clipboard
149    pub fn copy_to_clipboard(&mut self, text: &str) -> Result<(), String> {
150        if let Some(ref mut clipboard) = self.clipboard {
151            clipboard
152                .set_text(text.to_string())
153                .map_err(|e| format!("Failed to set clipboard text: {}", e))
154        } else {
155            Err("Clipboard not available".to_string())
156        }
157    }
158
159    /// Copy text to primary selection (Linux X11 only)
160    #[cfg(target_os = "linux")]
161    pub fn copy_to_primary_selection(&mut self, text: &str) -> Result<(), String> {
162        use arboard::SetExtLinux;
163
164        if let Some(ref mut clipboard) = self.clipboard {
165            clipboard
166                .set()
167                .clipboard(arboard::LinuxClipboardKind::Primary)
168                .text(text.to_string())
169                .map_err(|e| format!("Failed to set primary selection: {}", e))?;
170            Ok(())
171        } else {
172            Err("Clipboard not available".to_string())
173        }
174    }
175
176    /// Paste text from primary selection (Linux X11 only)
177    #[cfg(target_os = "linux")]
178    pub fn paste_from_primary_selection(&mut self) -> Option<Vec<u8>> {
179        use arboard::GetExtLinux;
180
181        if let Some(ref mut clipboard) = self.clipboard {
182            match clipboard
183                .get()
184                .clipboard(arboard::LinuxClipboardKind::Primary)
185                .text()
186            {
187                Ok(text) => {
188                    log::debug!("Pasting from primary selection: {} chars", text.len());
189                    // Convert newlines to carriage returns for terminal
190                    let text = text.replace('\n', "\r");
191                    Some(text.as_bytes().to_vec())
192                }
193                Err(e) => {
194                    log::error!("Failed to get primary selection text: {}", e);
195                    None
196                }
197            }
198        } else {
199            log::warn!("Clipboard not available");
200            None
201        }
202    }
203
204    /// Fallback for non-Linux platforms - copy to primary selection not supported
205    #[cfg(not(target_os = "linux"))]
206    pub fn copy_to_primary_selection(&mut self, _text: &str) -> Result<(), String> {
207        Ok(()) // No-op on non-Linux platforms
208    }
209
210    /// Fallback for non-Linux platforms - paste from primary selection uses regular clipboard
211    #[cfg(not(target_os = "linux"))]
212    pub fn paste_from_primary_selection(&mut self) -> Option<Vec<u8>> {
213        self.paste_from_clipboard()
214    }
215}
216
217impl Default for InputHandler {
218    fn default() -> Self {
219        Self::new()
220    }
221}