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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
// Copyright (c) 2025 R3BL LLC. Licensed under Apache License, Version 2.0.
//! Main PTY multiplexer orchestrator.
//!
//! This module provides the main `PTYMux` struct that coordinates all components
//! and manages the event loop for the terminal multiplexer.
use super::{InputRouter, OutputRenderer, Process, ProcessManager, output_renderer};
use crate::{AnsiSequenceGenerator, InputEvent, RawMode, Size, col,
core::{get_size,
osc::OscController,
pty::pty_core::pty_sessions::show_notification,
terminal_io::{InputDevice, OutputDevice}},
lock_output_device_as_mut, row};
/// Main PTY multiplexer that orchestrates all components.
pub struct PTYMux {
process_manager: ProcessManager,
input_router: InputRouter,
output_renderer: OutputRenderer,
terminal_size: Size,
output_device: OutputDevice,
input_device: InputDevice,
}
impl std::fmt::Debug for PTYMux {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PTYMux")
.field("process_manager", &self.process_manager)
.field("input_router", &self.input_router)
.field("output_renderer", &self.output_renderer)
.field("terminal_size", &self.terminal_size)
.field("output_device", &"<OutputDevice>")
.field("input_device", &"<InputDevice>")
.finish()
}
}
/// Builder for configuring and creating a `PTYMux` instance.
#[derive(Default, Debug)]
pub struct PTYMuxBuilder {
processes: Vec<Process>,
}
impl PTYMuxBuilder {
/// Set the processes to be managed by the multiplexer.
#[must_use]
pub fn processes(mut self, processes: Vec<Process>) -> Self {
self.processes = processes;
self
}
/// Add a single process to the multiplexer.
#[must_use]
pub fn add_process(mut self, process: Process) -> Self {
self.processes.push(process);
self
}
/// Build the `PTYMux` instance.
///
/// # Errors
///
/// Returns an error if no processes are configured or terminal setup fails.
pub fn build(self) -> miette::Result<PTYMux> {
if self.processes.is_empty() {
miette::bail!("At least one process must be configured");
}
if self.processes.len() > output_renderer::MAX_PROCESSES {
miette::bail!(
"Maximum of {} processes allowed",
output_renderer::MAX_PROCESSES
);
}
let terminal_size = get_size()?;
let output_device = OutputDevice::new_stdout();
let input_device = InputDevice::default();
Ok(PTYMux {
process_manager: ProcessManager::new(self.processes, terminal_size),
input_router: InputRouter::new(),
output_renderer: OutputRenderer::new(terminal_size),
terminal_size,
output_device,
input_device,
})
}
}
impl PTYMux {
/// Create a new builder for configuring a `PTYMux` instance.
#[must_use]
pub fn builder() -> PTYMuxBuilder { PTYMuxBuilder::default() }
/// Run the multiplexer event loop.
///
/// This is the main entry point that:
/// 1. Starts raw mode
/// 2. Sets initial terminal title
/// 3. Runs the main event loop
/// 4. Cleans up on exit
///
/// # Errors
///
/// Returns an error if terminal setup, process management, or event handling fails.
pub async fn run(mut self) -> miette::Result<()> {
// Start raw mode using existing RawMode.
RawMode::start(
self.terminal_size,
lock_output_device_as_mut!(&self.output_device),
false,
);
tracing::debug!("Raw mode started");
// Set initial terminal title using OSC controller.
{
let mut osc = OscController::new(&self.output_device);
osc.set_title_and_tab("PTYMux Example - Starting")?;
}
// Start all processes at startup.
tracing::debug!("Starting all processes");
self.process_manager.start_all_processes()?;
// Clear screen before showing first process.
{
let out = lock_output_device_as_mut!(&self.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();
}
// Trigger initial process switch to show first process.
self.process_manager.switch_to(0);
// Render initial status bar.
self.output_renderer
.render_initial_status_bar(&self.output_device, &self.process_manager)?;
// Main event loop
tracing::debug!("Starting main event loop");
let result = self.run_event_loop().await;
tracing::debug!("Main event loop exited with result: {:?}", result);
// Always cleanup regardless of error.
self.cleanup_terminal();
result
}
/// Main event loop that handles input and output events.
async fn run_event_loop(&mut self) -> miette::Result<()> {
// Create a periodic timer for status bar updates.
let mut status_bar_interval =
tokio::time::interval(tokio::time::Duration::from_millis(500));
// Create a fast timer for polling PTY output.
let mut output_poll_interval =
tokio::time::interval(tokio::time::Duration::from_millis(10));
'main_loop: loop {
tokio::select! {
// Poll ALL processes and update their virtual terminal buffers.
// https://developerlife.com/2024/07/10/rust-async-cancellation-safety-tokio/#example-1-right-and-wrong-way-to-sleep-and-interval
_ = output_poll_interval.tick() => {
// **Core of per-process virtual terminal architecture**:
// Poll ALL processes continuously (every 10ms), not just the active one.
// Each process updates its own OffscreenBuffer independently.
// This is what enables instant switching with full state preservation.
let active_had_output = self.process_manager.poll_all_processes();
// **Selective rendering optimization**:
// Only render when the currently visible process has new output.
// All other processes continue updating their virtual terminals.
// in the background, ready for instant switching.
if active_had_output {
// Get the active process's virtual terminal and render it.
self.output_renderer.render_from_active_buffer(
&self.output_device,
&self.process_manager
)?;
// Clear the "needs rendering" flag for the active process.
self.process_manager.mark_active_as_rendered();
}
}
// Handle user input using existing InputDevice.
Some(input_event) = self.input_device.next() => {
tracing::debug!("Received input event: {:?}", input_event);
// Show desktop notification for input event (filter out mouse events)
if !matches!(input_event, InputEvent::Mouse(_)) {
show_notification("PTY Mux - Input Event", &format!("Input event received: {input_event:?}"));
}
// Create OSC controller for this input handling.
let mut osc = OscController::new(&self.output_device);
// Handle input events using the input router.
tracing::debug!("Handling input event: {:?}", input_event);
let should_exit = self.input_router.handle_input(
input_event,
&mut self.process_manager,
&mut osc,
&self.output_device
)?;
if should_exit {
tracing::debug!("Exit requested by input router - breaking main event loop");
break 'main_loop; // Exit requested - break out of the loop
}
}
// Periodic status bar updates - ensures status bar is visible even when idle.
_ = status_bar_interval.tick() => {
self.output_renderer.render_initial_status_bar(&self.output_device, &self.process_manager)?;
}
}
}
tracing::debug!("Event loop completed - returning Ok(())");
Ok(())
}
/// Update terminal size for all components.
///
/// This method is called when the terminal is resized and ensures
/// all components are aware of the new size.
pub fn update_terminal_size(&mut self, new_size: Size) {
self.terminal_size = new_size;
self.process_manager.handle_terminal_resize(new_size);
self.output_renderer.update_terminal_size(new_size);
}
/// Get the current terminal size.
#[must_use]
pub fn terminal_size(&self) -> Size { self.terminal_size }
/// Get a reference to the process manager.
#[must_use]
pub fn process_manager(&self) -> &ProcessManager { &self.process_manager }
/// Get a mutable reference to the process manager.
pub fn process_manager_mut(&mut self) -> &mut ProcessManager {
&mut self.process_manager
}
/// Cleanup terminal state - always called on exit.
fn cleanup_terminal(&mut self) {
let start_time = std::time::Instant::now();
tracing::debug!("Starting cleanup - terminal size: {:?}", self.terminal_size);
// First, kill all running processes.
tracing::debug!("Step 1: Shutting down process manager");
self.process_manager.shutdown_all_processes();
tracing::debug!("Step 1 completed in {:?}", start_time.elapsed());
// Give processes a short time to terminate gracefully, then force exit.
tracing::debug!("Step 2: Waiting 100ms for processes to terminate gracefully");
std::thread::sleep(std::time::Duration::from_millis(100));
tracing::debug!("Step 2 completed in {:?}", start_time.elapsed());
// Force flush any pending output.
tracing::debug!("Step 3: Flushing pending output");
match lock_output_device_as_mut!(&self.output_device).flush() {
Ok(()) => tracing::debug!("Step 3: Output flush successful"),
Err(e) => tracing::warn!("Step 3: Output flush failed: {:?}", e),
}
tracing::debug!("Step 3 completed in {:?}", start_time.elapsed());
// Clear screen
tracing::debug!("Step 4: Clearing screen and homing cursor");
{
let out = lock_output_device_as_mut!(&self.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();
}
tracing::debug!("Step 4 completed in {:?}", start_time.elapsed());
// Force flush after escape sequences.
tracing::debug!("Step 5: Final output flush after escape sequences");
match lock_output_device_as_mut!(&self.output_device).flush() {
Ok(()) => tracing::debug!("Step 5: Final flush successful"),
Err(e) => tracing::warn!("Step 5: Final flush failed: {:?}", e),
}
tracing::debug!("Step 5 completed in {:?}", start_time.elapsed());
// End raw mode
tracing::debug!("Step 6: Ending raw mode");
RawMode::end(
self.terminal_size,
lock_output_device_as_mut!(&self.output_device),
false,
);
tracing::debug!("Step 6: Raw mode ended successfully");
let total_time = start_time.elapsed();
tracing::debug!("Cleanup completed successfully in {:?}", total_time);
if total_time > std::time::Duration::from_millis(500) {
tracing::warn!("Cleanup took longer than expected: {:?}", total_time);
}
// If cleanup took too long, there might be zombie processes.
// Force exit to prevent hanging.
if total_time > std::time::Duration::from_millis(1000) {
tracing::error!("Cleanup took over 1 second, forcing exit to prevent hang");
std::process::exit(0);
}
}
}