1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright (c) 2025 R3BL LLC. Licensed under Apache License, Version 2.0.
//! Dynamic input event routing for the PTY multiplexer.
//!
//! This module handles keyboard input routing, including dynamic process switching
//! shortcuts (F1 through F9 based on the number of processes) and
//! terminal resize events.
use super::ProcessManager;
use crate::{AnsiSequenceGenerator, FunctionKey, InputEvent, Key, KeyPress, KeyState,
ModifierKeysMask, Size, col,
core::{osc::OscController,
pty::{PtyInputEvent, pty_core::pty_sessions::show_notification},
terminal_io::OutputDevice},
lock_output_device_as_mut, row};
/// Routes input events to appropriate handlers and manages dynamic keyboard shortcuts.
#[derive(Debug)]
pub struct InputRouter;
impl InputRouter {
/// Create a new input router.
#[must_use]
pub fn new() -> Self { Self }
/// Handle an input event, routing it appropriately.
///
/// Returns `Ok(true)` if the application should exit, `Ok(false)` otherwise.
///
/// # Errors
///
/// Returns an error if terminal operations or process switching fails.
pub fn handle_input(
&mut self,
event: InputEvent,
process_manager: &mut ProcessManager,
osc: &mut OscController<'_>,
output_device: &OutputDevice,
) -> miette::Result<bool> {
match event {
InputEvent::Keyboard(key) => {
match key {
// Process switching: Handle F1 through F9 for switching processes
KeyPress::Plain {
key: Key::FunctionKey(fn_key),
} => {
let (fn_number, process_index) = match fn_key {
FunctionKey::F1 => (1, 0),
FunctionKey::F2 => (2, 1),
FunctionKey::F3 => (3, 2),
FunctionKey::F4 => (4, 3),
FunctionKey::F5 => (5, 4),
FunctionKey::F6 => (6, 5),
FunctionKey::F7 => (7, 6),
FunctionKey::F8 => (8, 7),
FunctionKey::F9 => (9, 8),
_ => return Ok(false), // F10-F12 not handled
};
tracing::debug!("Received F{} for process switching", fn_number);
// Only switch if the process index is valid for current process
// count.
if process_index < process_manager.processes().len() {
let old_index = process_manager.active_index();
if old_index == process_index {
tracing::debug!(
"F{} pressed but already on process {}",
fn_number,
process_index
);
} else {
tracing::debug!(
"Process switch: {} -> {} (triggered by F{})",
old_index,
process_index,
fn_number
);
// Show notification for process switching.
let process_name =
&process_manager.processes()[process_index].command;
show_notification(
"PTY Mux - Process Switch",
&format!("Switching to {process_name}"),
);
// Clear the screen before switching.
{
let out = lock_output_device_as_mut!(output_device);
let _unused = out.write_all(
AnsiSequenceGenerator::clear_screen().as_bytes(),
);
let _unused = out.write_all(
AnsiSequenceGenerator::cursor_position(
row(0),
col(0),
)
.as_bytes(),
);
let _unused = out.flush();
}
process_manager.switch_to(process_index);
Self::update_terminal_title(process_manager, osc)?;
tracing::debug!("Process switch completed successfully");
}
} else {
tracing::warn!(
"Invalid process index {} for current process count {} (F{} pressed)",
process_index,
process_manager.processes().len(),
fn_number
);
}
}
// Exit shortcut: Ctrl+Q
KeyPress::WithModifiers {
key: Key::Character('q'),
mask:
ModifierKeysMask {
ctrl_key_state: KeyState::Pressed,
shift_key_state: _, // Don't care about shift state
alt_key_state: _, // Don't care about alt state
},
} => {
tracing::debug!("Exit requested (Ctrl+Q)");
// Show notification for exit.
show_notification("PTY Mux - Exit", "Exiting PTY Mux");
return Ok(true); // Exit requested
}
_ => {
// Show notification for other key presses (useful for debugging)
show_notification(
"PTY Mux - Key Press",
&format!("Key pressed: {key:?}"),
);
// Forward all other input to active PTY using proper conversion.
if let Some(pty_event) = Option::<PtyInputEvent>::from(key) {
tracing::debug!("Forwarding input to PTY: {:?}", pty_event);
process_manager.send_input(pty_event)?;
}
}
}
}
InputEvent::Resize(new_size) => {
// Handle terminal resize - forward to all active PTYs.
Self::handle_resize(process_manager, new_size);
}
_ => {
// Other input events are ignored for now.
}
}
Ok(false)
}
/// Update the terminal title based on the currently active process.
fn update_terminal_title(
process_manager: &ProcessManager,
osc: &mut OscController<'_>,
) -> miette::Result<()> {
// Check if the active process has set a custom terminal title.
let title = if let Some(custom_title) = process_manager.active_terminal_title() {
// Use the process's custom title.
format!(
"PTYMux - {} - {}",
process_manager.active_name(),
custom_title
)
} else {
// Use default title with just process name.
format!("PTYMux - {}", process_manager.active_name())
};
osc.set_title_and_tab(&title)?;
Ok(())
}
/// Handle terminal resize events.
///
/// Updates the process manager's size and forwards reduced size to all PTY sessions
/// to reserve space for the status bar.
fn handle_resize(process_manager: &mut ProcessManager, new_size: Size) {
// Update manager's size (full terminal size)
process_manager.handle_terminal_resize(new_size);
// Forward reduced size to all PTY sessions to reserve status bar space.
// Note: The process manager handles the actual PTY size conversion
// We just need to update the manager with the full terminal size.
// The process manager handles forwarding resize events to PTY sessions
// so we don't need to do it here explicitly.
}
}
impl Default for InputRouter {
fn default() -> Self { Self::new() }
}