boxmux_lib/
input_loop.rs

1use crate::utils::should_use_pty;
2use crate::{handle_keypress, AppContext, FieldUpdate};
3use crate::{run_script, thread_manager::Runnable};
4use crossterm::event::{
5    poll, read, Event, KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind,
6};
7use std::sync::mpsc;
8use std::time::Duration;
9
10use crate::thread_manager::*;
11
12use uuid::Uuid;
13
14/// Convert crossterm KeyEvent to appropriate PTY input string
15/// F0127: Enhanced special key handling with modifiers and extended key support
16pub fn format_key_for_pty(code: KeyCode, modifiers: KeyModifiers) -> String {
17    // Helper function to generate modified key sequences
18    let format_modified_key = |base_seq: &str, modifiers: KeyModifiers| -> String {
19        let mut mod_code = 1;
20        if modifiers.contains(KeyModifiers::SHIFT) {
21            mod_code += 1;
22        }
23        if modifiers.contains(KeyModifiers::ALT) {
24            mod_code += 2;
25        }
26        if modifiers.contains(KeyModifiers::CONTROL) {
27            mod_code += 4;
28        }
29
30        if mod_code == 1 {
31            base_seq.to_string()
32        } else {
33            // Format: ESC[1;{mod}{key}
34            format!("\x1b[1;{}{}", mod_code, &base_seq[2..])
35        }
36    };
37
38    match code {
39        KeyCode::Char(c) => {
40            if modifiers.contains(KeyModifiers::CONTROL) {
41                // Enhanced control character handling
42                match c {
43                    'a'..='z' => {
44                        let ctrl_code = (c as u8) - b'a' + 1;
45                        format!("{}", ctrl_code as char)
46                    }
47                    'A'..='Z' => {
48                        let ctrl_code = (c as u8) - b'A' + 1;
49                        format!("{}", ctrl_code as char)
50                    }
51                    '@' => "\x00".to_string(),  // Ctrl+@
52                    '[' => "\x1b".to_string(),  // Ctrl+[
53                    '\\' => "\x1c".to_string(), // Ctrl+\
54                    ']' => "\x1d".to_string(),  // Ctrl+]
55                    '^' => "\x1e".to_string(),  // Ctrl+^
56                    '_' => "\x1f".to_string(),  // Ctrl+_
57                    ' ' => "\x00".to_string(),  // Ctrl+Space
58                    _ => c.to_string(),
59                }
60            } else if modifiers.contains(KeyModifiers::ALT) {
61                // Alt+character combinations
62                format!("\x1b{}", c)
63            } else {
64                c.to_string()
65            }
66        }
67        KeyCode::Enter => {
68            if modifiers.contains(KeyModifiers::CONTROL) {
69                "\n".to_string() // Ctrl+Enter
70            } else {
71                "\r".to_string()
72            }
73        }
74        KeyCode::Tab => {
75            if modifiers.contains(KeyModifiers::SHIFT) {
76                "\x1b[Z".to_string() // Shift+Tab (Back Tab)
77            } else {
78                "\t".to_string()
79            }
80        }
81        KeyCode::Backspace => {
82            if modifiers.contains(KeyModifiers::CONTROL) {
83                "\x08".to_string() // Ctrl+Backspace
84            } else if modifiers.contains(KeyModifiers::ALT) {
85                "\x1b\x7f".to_string() // Alt+Backspace
86            } else {
87                "\x7f".to_string()
88            }
89        }
90        KeyCode::Delete => format_modified_key("\x1b[3~", modifiers),
91        KeyCode::Insert => format_modified_key("\x1b[2~", modifiers),
92
93        // Arrow keys with modifier support
94        KeyCode::Up => format_modified_key("\x1b[A", modifiers),
95        KeyCode::Down => format_modified_key("\x1b[B", modifiers),
96        KeyCode::Right => format_modified_key("\x1b[C", modifiers),
97        KeyCode::Left => format_modified_key("\x1b[D", modifiers),
98
99        // Home/End with modifier support
100        KeyCode::Home => format_modified_key("\x1b[H", modifiers),
101        KeyCode::End => format_modified_key("\x1b[F", modifiers),
102
103        // Page Up/Down with modifier support
104        KeyCode::PageUp => format_modified_key("\x1b[5~", modifiers),
105        KeyCode::PageDown => format_modified_key("\x1b[6~", modifiers),
106
107        KeyCode::Esc => "\x1b".to_string(),
108
109        KeyCode::F(n) => {
110            // F1-F24 keys with modifier support
111            let base_seq = match n {
112                1 => "\x1bOP",
113                2 => "\x1bOQ",
114                3 => "\x1bOR",
115                4 => "\x1bOS",
116                5 => "\x1b[15~",
117                6 => "\x1b[17~",
118                7 => "\x1b[18~",
119                8 => "\x1b[19~",
120                9 => "\x1b[20~",
121                10 => "\x1b[21~",
122                11 => "\x1b[23~",
123                12 => "\x1b[24~",
124                13 => "\x1b[25~",
125                14 => "\x1b[26~",
126                15 => "\x1b[28~",
127                16 => "\x1b[29~",
128                17 => "\x1b[31~",
129                18 => "\x1b[32~",
130                19 => "\x1b[33~",
131                20 => "\x1b[34~",
132                21 => "\x1b[35~",
133                22 => "\x1b[36~",
134                23 => "\x1b[37~",
135                24 => "\x1b[38~",
136                _ => return "".to_string(),
137            };
138
139            if modifiers.is_empty() {
140                base_seq.to_string()
141            } else {
142                format_modified_key(base_seq, modifiers)
143            }
144        }
145
146        // Additional special keys
147        KeyCode::CapsLock => "".to_string(), // Usually handled by system
148        KeyCode::ScrollLock => "".to_string(),
149        KeyCode::NumLock => "".to_string(),
150        KeyCode::PrintScreen => "".to_string(),
151        KeyCode::Pause => "".to_string(),
152        KeyCode::Menu => "\x1b[29~".to_string(),
153
154        _ => "".to_string(),
155    }
156}
157create_runnable!(
158    InputLoop,
159    |_inner: &mut RunnableImpl, _app_context: AppContext, _messages: Vec<Message>| -> bool { true },
160    |inner: &mut RunnableImpl,
161     app_context: AppContext,
162     _messages: Vec<Message>|
163     -> (bool, AppContext) {
164        let mut should_continue = true;
165
166        let active_layout = app_context.app.get_active_layout().unwrap();
167
168        if poll(Duration::from_millis(10)).unwrap() {
169            if let Ok(event) = read() {
170                let key_str = match event {
171                    Event::Mouse(MouseEvent {
172                        kind,
173                        column,
174                        row,
175                        modifiers: _,
176                    }) => {
177                        match kind {
178                            MouseEventKind::ScrollUp => {
179                                inner.send_message(Message::ScrollMuxBoxUp());
180                                "ScrollUp".to_string()
181                            }
182                            MouseEventKind::ScrollDown => {
183                                inner.send_message(Message::ScrollMuxBoxDown());
184                                "ScrollDown".to_string()
185                            }
186                            MouseEventKind::ScrollLeft => {
187                                inner.send_message(Message::ScrollMuxBoxLeft());
188                                "ScrollLeft".to_string()
189                            }
190                            MouseEventKind::ScrollRight => {
191                                inner.send_message(Message::ScrollMuxBoxRight());
192                                "ScrollRight".to_string()
193                            }
194                            MouseEventKind::Down(_button) => {
195                                // F0091 & F0188: Handle mouse clicks and start of drag
196                                inner.send_message(Message::MouseClick(column, row));
197                                inner.send_message(Message::MouseDragStart(column, row));
198                                format!("MouseClick({}, {})", column, row)
199                            }
200                            MouseEventKind::Drag(_button) => {
201                                // F0188: Handle mouse drag for scroll knob dragging
202                                inner.send_message(Message::MouseDrag(column, row));
203                                format!("MouseDrag({}, {})", column, row)
204                            }
205                            MouseEventKind::Up(_button) => {
206                                // F0188: Handle end of drag
207                                inner.send_message(Message::MouseDragEnd(column, row));
208                                format!("MouseDragEnd({}, {})", column, row)
209                            }
210                            _ => return (true, app_context), // Ignore other mouse events
211                        }
212                    }
213                    Event::Key(KeyEvent {
214                        code, modifiers, ..
215                    }) => {
216                        // Check if focused muxbox has PTY enabled - if so, route input to PTY
217                        let selected_muxboxes = active_layout.get_selected_muxboxes();
218                        let focused_muxbox_has_pty = selected_muxboxes
219                            .first()
220                            .map(|muxbox| should_use_pty(muxbox))
221                            .unwrap_or(false);
222
223                        if focused_muxbox_has_pty {
224                            // Convert key event to string and send to PTY
225                            let key_str = format_key_for_pty(code, modifiers);
226                            if let Some(focused_muxbox) = selected_muxboxes.first() {
227                                inner.send_message(Message::PTYInput(
228                                    focused_muxbox.id.clone(),
229                                    key_str.clone(),
230                                ));
231                                return (true, app_context);
232                            }
233                        }
234
235                        match code {
236                            KeyCode::Char('q') => {
237                                inner.send_message(Message::Exit);
238                                should_continue = false; // Stop running
239                                "q".to_string()
240                            }
241                            KeyCode::Tab => {
242                                inner.send_message(Message::NextMuxBox());
243                                "Tab".to_string()
244                            }
245                            KeyCode::BackTab => {
246                                inner.send_message(Message::PreviousMuxBox());
247                                "BackTab".to_string()
248                            }
249                            KeyCode::Enter => "Enter".to_string(),
250                            KeyCode::Down => {
251                                inner.send_message(Message::ScrollMuxBoxDown());
252                                "Down".to_string()
253                            }
254                            KeyCode::Up => {
255                                inner.send_message(Message::ScrollMuxBoxUp());
256                                "Up".to_string()
257                            }
258                            KeyCode::Left => {
259                                inner.send_message(Message::ScrollMuxBoxLeft());
260                                "Left".to_string()
261                            }
262                            KeyCode::Right => {
263                                inner.send_message(Message::ScrollMuxBoxRight());
264                                "Right".to_string()
265                            }
266                            KeyCode::PageUp => {
267                                if modifiers.contains(KeyModifiers::SHIFT) {
268                                    inner.send_message(Message::ScrollMuxBoxPageLeft());
269                                    "Shift+PageUp".to_string()
270                                } else {
271                                    inner.send_message(Message::ScrollMuxBoxPageUp());
272                                    "PageUp".to_string()
273                                }
274                            }
275                            KeyCode::PageDown => {
276                                if modifiers.contains(KeyModifiers::SHIFT) {
277                                    inner.send_message(Message::ScrollMuxBoxPageRight());
278                                    "Shift+PageDown".to_string()
279                                } else {
280                                    inner.send_message(Message::ScrollMuxBoxPageDown());
281                                    "PageDown".to_string()
282                                }
283                            }
284                            KeyCode::Char(c) => {
285                                if modifiers.contains(KeyModifiers::CONTROL) {
286                                    match c {
287                                        'c' => {
288                                            inner.send_message(Message::CopyFocusedMuxBoxContent());
289                                            "Ctrl+c".to_string()
290                                        }
291                                        _ => format!("Ctrl+{}", c),
292                                    }
293                                } else if modifiers.contains(KeyModifiers::SUPER) {
294                                    match c {
295                                        'c' => {
296                                            inner.send_message(Message::CopyFocusedMuxBoxContent());
297                                            "Cmd+c".to_string()
298                                        }
299                                        _ => format!("Cmd+{}", c),
300                                    }
301                                } else if modifiers.contains(KeyModifiers::ALT) {
302                                    format!("Alt+{}", c)
303                                } else {
304                                    c.to_string()
305                                }
306                            }
307                            KeyCode::Backspace => "Backspace".to_string(),
308                            KeyCode::Delete => "Delete".to_string(),
309                            KeyCode::Esc => "Esc".to_string(),
310                            KeyCode::Home => {
311                                if modifiers.contains(KeyModifiers::CONTROL) {
312                                    // Ctrl+Home: scroll to top vertically
313                                    inner.send_message(Message::ScrollMuxBoxToTop());
314                                    "Ctrl+Home".to_string()
315                                } else {
316                                    // Home: scroll to beginning horizontally
317                                    inner.send_message(Message::ScrollMuxBoxToBeginning());
318                                    "Home".to_string()
319                                }
320                            }
321                            KeyCode::End => {
322                                if modifiers.contains(KeyModifiers::CONTROL) {
323                                    // Ctrl+End: scroll to bottom vertically
324                                    inner.send_message(Message::ScrollMuxBoxToBottom());
325                                    "Ctrl+End".to_string()
326                                } else {
327                                    // End: scroll to end horizontally
328                                    inner.send_message(Message::ScrollMuxBoxToEnd());
329                                    "End".to_string()
330                                }
331                            }
332                            KeyCode::F(n) => format!("F{}", n),
333                            KeyCode::Insert => "Insert".to_string(),
334                            _ => return (true, app_context),
335                        }
336                    }
337                    _ => return (true, app_context),
338                };
339
340                // F0081: Hot Key Actions - Direct choice execution
341                if let Some(hot_keys) = &app_context.app.hot_keys {
342                    if let Some(choice_id) = hot_keys.get(&key_str) {
343                        inner.send_message(Message::ExecuteHotKeyChoice(choice_id.clone()));
344                    }
345                }
346
347                if let Some(app_key_mappings) = &app_context.app.on_keypress {
348                    if let Some(actions) = handle_keypress(&key_str, app_key_mappings) {
349                        let libs = app_context.app.libs.clone();
350                        let _result = run_script(libs, &actions);
351                    }
352                }
353                if let Some(layout_key_mappings) = &active_layout.on_keypress {
354                    if let Some(actions) = handle_keypress(&key_str, layout_key_mappings) {
355                        let libs = app_context.app.libs.clone();
356                        let _ = run_script(libs, &actions);
357                    }
358                }
359
360                inner.send_message(Message::KeyPress(key_str.clone()));
361            }
362        }
363        std::thread::sleep(std::time::Duration::from_millis(
364            app_context.config.frame_delay,
365        ));
366
367        (should_continue, app_context)
368    }
369);