bubbletea_rs/program.rs
1//! This module defines the `Program` struct and its associated `ProgramBuilder`,
2//! which are responsible for coordinating the entire `bubbletea-rs` application lifecycle.
3//! The `Program` sets up the terminal, handles input, executes commands, and renders
4//! the model's view.
5
6use crate::event::{KillMsg, RequestWindowSizeMsg};
7use crate::{
8 Error, InputHandler, InputSource, Model, Msg, QuitMsg, Terminal, TerminalInterface,
9 WindowSizeMsg,
10};
11use futures::{future::FutureExt, select};
12use std::marker::PhantomData;
13use std::panic;
14use std::sync::OnceLock;
15use tokio::sync::mpsc;
16
17type PanicHook = Box<dyn Fn(&panic::PanicHookInfo<'_>) + Send + Sync + 'static>;
18static ORIGINAL_PANIC_HOOK: OnceLock<PanicHook> = OnceLock::new();
19
20/// Defines the different modes for mouse motion reporting.
21#[derive(Debug, Clone, Copy)]
22pub enum MouseMotion {
23 /// No mouse motion events are reported.
24 None,
25 /// Mouse motion events are reported when the mouse moves over a different cell.
26 Cell,
27 /// Mouse motion events are reported for every pixel movement.
28 All,
29}
30
31use std::collections::HashMap;
32use std::sync::Arc;
33use tokio::io::AsyncWrite;
34use tokio::sync::Mutex;
35use tokio::task::JoinSet;
36use tokio_util::sync::CancellationToken;
37
38/// Alias for a model-aware message filter function used throughout Program.
39///
40/// This reduces repeated complex type signatures and improves readability.
41type MessageFilter<M> = Box<dyn Fn(&M, Msg) -> Option<Msg> + Send>;
42
43/// Configuration options for a `Program`.
44///
45/// This struct holds various settings that control the behavior of the `Program`,
46/// such as terminal features, rendering options, and panic/signal handling.
47pub struct ProgramConfig {
48 /// Whether to use the alternate screen buffer.
49 pub alt_screen: bool,
50 /// The mouse motion reporting mode.
51 pub mouse_motion: MouseMotion,
52 /// Whether to report focus events.
53 pub report_focus: bool,
54 /// The target frames per second for rendering.
55 pub fps: u32,
56 /// Whether to disable the renderer entirely.
57 pub without_renderer: bool,
58 /// Whether to catch panics and convert them into `ProgramPanic` errors.
59 pub catch_panics: bool,
60 /// Whether to enable signal handling (e.g., Ctrl+C).
61 pub signal_handler: bool,
62 /// Whether to enable bracketed paste mode.
63 pub bracketed_paste: bool,
64 /// Optional custom output writer.
65 pub output_writer: Option<Arc<Mutex<dyn AsyncWrite + Send + Unpin>>>,
66 /// Optional cancellation token for external control.
67 pub cancellation_token: Option<CancellationToken>,
68 // Message filter is model-aware and stored on Program<M> instead of in ProgramConfig
69 /// Optional custom input source.
70 pub input_source: Option<InputSource>,
71 /// The buffer size for the event channel (None for unbounded, Some(size) for bounded).
72 pub event_channel_buffer: Option<usize>,
73 /// Whether to enable memory usage monitoring.
74 pub memory_monitoring: bool,
75 /// Optional environment variables to apply to external process commands.
76 pub environment: Option<HashMap<String, String>>,
77}
78
79impl std::fmt::Debug for ProgramConfig {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("ProgramConfig")
82 .field("alt_screen", &self.alt_screen)
83 .field("mouse_motion", &self.mouse_motion)
84 .field("report_focus", &self.report_focus)
85 .field("fps", &self.fps)
86 .field("without_renderer", &self.without_renderer)
87 .field("catch_panics", &self.catch_panics)
88 .field("signal_handler", &self.signal_handler)
89 .field("bracketed_paste", &self.bracketed_paste)
90 .field("cancellation_token", &self.cancellation_token)
91 .field("environment", &self.environment.as_ref().map(|m| m.len()))
92 .finish()
93 }
94}
95
96impl Default for ProgramConfig {
97 /// Returns the default `ProgramConfig`.
98 ///
99 /// By default, the program does not use the alternate screen, has no mouse
100 /// motion reporting, does not report focus, targets 60 FPS, enables rendering,
101 /// catches panics, handles signals, and disables bracketed paste.
102 fn default() -> Self {
103 Self {
104 alt_screen: false,
105 mouse_motion: MouseMotion::None,
106 report_focus: false,
107 fps: 60,
108 without_renderer: false,
109 catch_panics: true,
110 signal_handler: true,
111 bracketed_paste: false,
112 output_writer: None,
113 cancellation_token: None,
114 input_source: None,
115 event_channel_buffer: Some(1000), // Default to bounded channel with 1000 message buffer
116 memory_monitoring: false, // Disabled by default
117 environment: None,
118 }
119 }
120}
121
122/// A builder for creating and configuring `Program` instances.
123///
124/// The `ProgramBuilder` provides a fluent API for setting various configuration
125/// options before building the final `Program`.
126pub struct ProgramBuilder<M: Model> {
127 config: ProgramConfig,
128 _phantom: PhantomData<M>,
129 /// Optional model-aware message filter
130 message_filter: Option<MessageFilter<M>>,
131}
132
133impl<M: Model> ProgramBuilder<M> {
134 /// Creates a new `ProgramBuilder` with default configuration.
135 ///
136 /// This method is used internally by `Program::builder()` and should not
137 /// be called directly. Use `Program::builder()` instead.
138 ///
139 /// # Returns
140 ///
141 /// A new `ProgramBuilder` instance with default settings.
142 pub(crate) fn new() -> Self {
143 Self {
144 config: ProgramConfig::default(),
145 _phantom: PhantomData,
146 message_filter: None,
147 }
148 }
149
150 /// Sets environment variables to apply to external process commands created
151 /// via `command::exec_process`.
152 ///
153 /// These environment variables will be merged with the system environment
154 /// when spawning external processes through commands.
155 ///
156 /// # Arguments
157 ///
158 /// * `env` - A `HashMap` of environment variable key-value pairs.
159 ///
160 /// # Example
161 ///
162 /// ```rust
163 /// use std::collections::HashMap;
164 /// use bubbletea_rs::Program;
165 /// # use bubbletea_rs::Model;
166 /// # struct MyModel;
167 /// # impl Model for MyModel {
168 /// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (MyModel, None) }
169 /// # fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
170 /// # fn view(&self) -> String { String::new() }
171 /// # }
172 ///
173 /// let mut env = HashMap::new();
174 /// env.insert("CUSTOM_VAR".to_string(), "value".to_string());
175 ///
176 /// let program = Program::<MyModel>::builder()
177 /// .with_environment(env)
178 /// .build();
179 /// ```
180 pub fn with_environment(mut self, env: HashMap<String, String>) -> Self {
181 self.config.environment = Some(env);
182 self
183 }
184
185 /// Sets whether to use the alternate screen buffer.
186 ///
187 /// When enabled, the application will run in an alternate screen buffer,
188 /// preserving the main terminal content.
189 pub fn alt_screen(mut self, enabled: bool) -> Self {
190 self.config.alt_screen = enabled;
191 self
192 }
193
194 /// Sets the mouse motion reporting mode.
195 ///
196 /// # Arguments
197 ///
198 /// * `motion` - The desired `MouseMotion` mode.
199 pub fn mouse_motion(mut self, motion: MouseMotion) -> Self {
200 self.config.mouse_motion = motion;
201 self
202 }
203
204 /// Sets whether to report focus events.
205 ///
206 /// When enabled, the application will receive `FocusMsg` and `BlurMsg`
207 /// when the terminal gains or loses focus.
208 pub fn report_focus(mut self, enabled: bool) -> Self {
209 self.config.report_focus = enabled;
210 self
211 }
212
213 /// Sets the target frames per second for rendering.
214 ///
215 /// This controls how often the `view` method of the model is called and
216 /// the terminal is updated.
217 ///
218 /// # Arguments
219 ///
220 /// * `fps` - The target frames per second.
221 pub fn with_fps(mut self, fps: u32) -> Self {
222 self.config.fps = fps;
223 self
224 }
225
226 /// Disables the renderer.
227 ///
228 /// When disabled, the `view` method will not be called and no output
229 /// will be rendered to the terminal. This is useful for testing or
230 /// headless operations.
231 pub fn without_renderer(mut self) -> Self {
232 self.config.without_renderer = true;
233 self
234 }
235
236 /// Sets whether to catch panics.
237 ///
238 /// When enabled, application panics will be caught and converted into
239 /// `ProgramPanic` errors, allowing for graceful shutdown.
240 pub fn catch_panics(mut self, enabled: bool) -> Self {
241 self.config.catch_panics = enabled;
242 self
243 }
244
245 /// Sets whether to enable signal handling.
246 ///
247 /// When enabled, the `Program` will listen for OS signals (e.g., Ctrl+C)
248 /// and attempt a graceful shutdown.
249 pub fn signal_handler(mut self, enabled: bool) -> Self {
250 self.config.signal_handler = enabled;
251 self
252 }
253
254 /// Sets whether to enable bracketed paste mode.
255 ///
256 /// When enabled, pasted text will be wrapped in special escape sequences,
257 /// allowing the application to distinguish pasted input from typed input.
258 pub fn bracketed_paste(mut self, enabled: bool) -> Self {
259 self.config.bracketed_paste = enabled;
260 self
261 }
262
263 /// Configures the program to use the default terminal input (stdin).
264 ///
265 /// This is the default behavior, so calling this method is optional.
266 /// It's provided for explicit configuration when needed.
267 ///
268 /// # Returns
269 ///
270 /// The `ProgramBuilder` instance for method chaining.
271 pub fn input_tty(self) -> Self {
272 // No-op for now, as stdin is used by default
273 self
274 }
275
276 /// Sets a custom input reader for the program.
277 ///
278 /// # Arguments
279 ///
280 /// * `reader` - A custom input stream that implements `tokio::io::AsyncRead + Send + Unpin`.
281 pub fn input(mut self, reader: impl tokio::io::AsyncRead + Send + Unpin + 'static) -> Self {
282 self.config.input_source = Some(InputSource::Custom(Box::pin(reader)));
283 self
284 }
285
286 /// Sets a custom output writer for the program.
287 ///
288 /// # Arguments
289 ///
290 /// * `writer` - A custom output stream that implements `tokio::io::AsyncWrite + Send + Unpin`.
291 pub fn output(mut self, writer: impl AsyncWrite + Send + Unpin + 'static) -> Self {
292 self.config.output_writer = Some(Arc::new(Mutex::new(Box::new(writer))));
293 self
294 }
295
296 /// Sets an external cancellation token for the program.
297 ///
298 /// When the token is cancelled, the program's event loop will gracefully shut down.
299 ///
300 /// # Arguments
301 ///
302 /// * `token` - The `CancellationToken` to use for external cancellation.
303 pub fn context(mut self, token: CancellationToken) -> Self {
304 self.config.cancellation_token = Some(token);
305 self
306 }
307
308 /// Sets a model-aware message filter function.
309 ///
310 /// The provided closure will be called for each incoming message with access
311 /// to the current model, allowing for context-aware transformation or filtering.
312 ///
313 /// # Arguments
314 ///
315 /// * `f` - A closure that takes `&M` and `Msg`, returning an `Option<Msg>`.
316 pub fn filter(mut self, f: impl Fn(&M, Msg) -> Option<Msg> + Send + 'static) -> Self {
317 self.message_filter = Some(Box::new(f));
318 self
319 }
320
321 /// Sets the event channel buffer size.
322 ///
323 /// By default, the channel has a buffer of 1000 messages. Setting this to `None`
324 /// will use an unbounded channel (not recommended for production), while setting
325 /// it to `Some(size)` will use a bounded channel with the specified buffer size.
326 ///
327 /// # Arguments
328 ///
329 /// * `buffer_size` - The buffer size for the event channel.
330 pub fn event_channel_buffer(mut self, buffer_size: Option<usize>) -> Self {
331 self.config.event_channel_buffer = buffer_size;
332 self
333 }
334
335 /// Enables memory usage monitoring.
336 ///
337 /// When enabled, the program will track memory usage metrics that can be
338 /// accessed for debugging and performance analysis.
339 pub fn memory_monitoring(mut self, enabled: bool) -> Self {
340 self.config.memory_monitoring = enabled;
341 self
342 }
343
344 /// Builds the `Program` instance with the configured options.
345 ///
346 /// # Returns
347 ///
348 /// A `Result` containing the `Program` instance or an `Error` if building fails.
349 pub fn build(self) -> Result<Program<M>, Error> {
350 Program::new(self.config, self.message_filter)
351 }
352}
353
354/// The main `Program` struct that coordinates the application.
355///
356/// The `Program` is responsible for setting up the terminal, managing the
357/// event loop, executing commands, and rendering the model's view.
358pub struct Program<M: Model> {
359 /// The configuration for this `Program` instance.
360 pub config: ProgramConfig,
361 event_tx: crate::event::EventSender,
362 event_rx: crate::event::EventReceiver,
363 terminal: Option<Box<dyn TerminalInterface + Send>>,
364 /// Active timer handles for cancellation
365 active_timers: HashMap<u64, CancellationToken>,
366 /// Set of spawned tasks that can be cancelled on shutdown
367 task_set: JoinSet<()>,
368 /// Cancellation token for coordinated shutdown
369 shutdown_token: CancellationToken,
370 /// Memory usage monitor (optional)
371 memory_monitor: Option<crate::memory::MemoryMonitor>,
372 /// Optional model-aware message filter
373 message_filter: Option<MessageFilter<M>>,
374 _phantom: PhantomData<M>,
375}
376
377impl<M: Model> Program<M> {
378 /// Creates a new `ProgramBuilder` for configuring and building a `Program`.
379 pub fn builder() -> ProgramBuilder<M> {
380 ProgramBuilder::new()
381 }
382
383 /// Creates a new `Program` instance with the given configuration.
384 ///
385 /// This method is called internally by `ProgramBuilder::build()` and should not
386 /// be called directly. Use `Program::builder()` followed by `build()` instead.
387 ///
388 /// # Arguments
389 ///
390 /// * `config` - The `ProgramConfig` to use for this program.
391 /// * `message_filter` - Optional model-aware message filter function.
392 ///
393 /// # Returns
394 ///
395 /// A `Result` containing the `Program` instance or an `Error` if initialization fails.
396 ///
397 /// # Errors
398 ///
399 /// Returns an `Error` if:
400 /// - Terminal initialization fails
401 /// - Event channel setup fails
402 /// - Global state initialization fails
403 fn new(config: ProgramConfig, message_filter: Option<MessageFilter<M>>) -> Result<Self, Error> {
404 let (event_tx, event_rx) = if let Some(buffer_size) = config.event_channel_buffer {
405 let (tx, rx) = mpsc::channel(buffer_size);
406 (
407 crate::event::EventSender::Bounded(tx),
408 crate::event::EventReceiver::Bounded(rx),
409 )
410 } else {
411 let (tx, rx) = mpsc::unbounded_channel();
412 (
413 crate::event::EventSender::Unbounded(tx),
414 crate::event::EventReceiver::Unbounded(rx),
415 )
416 };
417
418 let terminal = if config.without_renderer {
419 None
420 } else {
421 let output_writer_for_terminal = config.output_writer.clone();
422 Some(Box::new(Terminal::new(output_writer_for_terminal)?)
423 as Box<dyn TerminalInterface + Send>)
424 };
425
426 // Expose the event sender globally for command helpers
427 let _ = crate::event::EVENT_SENDER.set(event_tx.clone());
428
429 // Expose command environment globally for exec_process
430 let _ = crate::command::COMMAND_ENV.set(config.environment.clone().unwrap_or_default());
431
432 let memory_monitor = if config.memory_monitoring {
433 Some(crate::memory::MemoryMonitor::new())
434 } else {
435 None
436 };
437
438 Ok(Self {
439 config,
440 event_tx,
441 event_rx,
442 terminal,
443 active_timers: HashMap::new(),
444 task_set: JoinSet::new(),
445 shutdown_token: CancellationToken::new(),
446 memory_monitor,
447 message_filter,
448 _phantom: PhantomData,
449 })
450 }
451
452 /// Runs the `bubbletea-rs` application.
453 ///
454 /// This method initializes the terminal, starts the event loop, and manages
455 /// the application's lifecycle. It will continue to run until a `QuitMsg`
456 /// is received or an unrecoverable error occurs.
457 ///
458 /// # Returns
459 ///
460 /// A `Result` containing the final `Model` state or an `Error` if the program
461 /// terminates abnormally.
462 pub async fn run(mut self) -> Result<M, Error> {
463 // Set up panic hook
464 if self.config.catch_panics {
465 let event_tx = self.event_tx.clone();
466 ORIGINAL_PANIC_HOOK.get_or_init(|| panic::take_hook());
467
468 panic::set_hook(Box::new(move |panic_info| {
469 let payload = if let Some(s) = panic_info.payload().downcast_ref::<&str>() {
470 s.to_string()
471 } else if let Some(s) = panic_info.payload().downcast_ref::<String>() {
472 s.clone()
473 } else {
474 "<unknown panic>".to_string()
475 };
476 let _ = event_tx.send(Box::new(crate::Error::ProgramPanic(payload)) as Msg);
477
478 // Call the original hook if it exists
479 if let Some(hook) = ORIGINAL_PANIC_HOOK.get() {
480 hook(panic_info);
481 }
482 }));
483 }
484
485 // Setup terminal
486 if let Some(terminal) = &mut self.terminal {
487 terminal.enter_raw_mode().await?;
488 if self.config.alt_screen {
489 terminal.enter_alt_screen().await?;
490 }
491 match self.config.mouse_motion {
492 MouseMotion::Cell => terminal.enable_mouse_cell_motion().await?,
493 MouseMotion::All => terminal.enable_mouse_all_motion().await?,
494 MouseMotion::None => (),
495 }
496 if self.config.report_focus {
497 terminal.enable_focus_reporting().await?;
498 }
499 if self.config.bracketed_paste {
500 terminal.enable_bracketed_paste().await?;
501 }
502 terminal.hide_cursor().await?;
503 }
504
505 let (mut model, mut cmd) = M::init();
506
507 // Setup input handling - either terminal input or custom input source
508 if self.terminal.is_some() || self.config.input_source.is_some() {
509 let input_source = self.config.input_source.take();
510 let input_handler = if let Some(source) = input_source {
511 InputHandler::with_source(self.event_tx.clone(), source)
512 } else {
513 InputHandler::new(self.event_tx.clone())
514 };
515 let shutdown_token = self.shutdown_token.clone();
516
517 // Update memory monitoring
518 if let Some(ref monitor) = self.memory_monitor {
519 monitor.task_spawned();
520 }
521
522 self.task_set.spawn(async move {
523 tokio::select! {
524 _ = shutdown_token.cancelled() => {
525 // Shutdown requested
526 }
527 _ = input_handler.run() => {
528 // Input handler completed
529 }
530 }
531 });
532 }
533
534 let result = 'main_loop: loop {
535 if let Some(c) = cmd.take() {
536 let event_tx = self.event_tx.clone();
537 let shutdown_token = self.shutdown_token.clone();
538
539 // Update memory monitoring
540 if let Some(ref monitor) = self.memory_monitor {
541 monitor.task_spawned();
542 }
543
544 self.task_set.spawn(async move {
545 tokio::select! {
546 _ = shutdown_token.cancelled() => {
547 // Shutdown requested, don't process command
548 }
549 result = c => {
550 if let Some(msg) = result {
551 let _ = event_tx.send(msg);
552 }
553 }
554 }
555 });
556 }
557
558 select! {
559 _ = self.config.cancellation_token.as_ref().map_or(futures::future::pending().left_future(), |token| token.cancelled().right_future()).fuse() => {
560 break Ok(model); // External cancellation
561 }
562 event = self.event_rx.recv().fuse() => {
563 if let Some(mut msg) = event {
564 // KillMsg triggers immediate termination without touching the model
565 if msg.downcast_ref::<KillMsg>().is_some() {
566 break Err(Error::ProgramKilled);
567 }
568 if let Some(filter_fn) = &self.message_filter {
569 if let Some(filtered_msg) = filter_fn(&model, msg) {
570 msg = filtered_msg;
571 } else {
572 continue; // Message was filtered out
573 }
574 }
575 // If the filter produced a KillMsg, terminate immediately
576 if msg.downcast_ref::<KillMsg>().is_some() {
577 break Err(Error::ProgramKilled);
578 }
579 // Check for special internal messages
580 let mut should_quit = false;
581 let mut should_interrupt = false;
582
583 // Handle special internal messages that need to consume the message
584 if msg.is::<crate::event::ClearScreenMsg>() {
585 if let Some(terminal) = &mut self.terminal {
586 let _ = terminal.clear().await;
587 }
588 continue; // handled; don't pass to the model
589 } else if msg.is::<crate::event::EnterAltScreenMsg>() {
590 if let Some(terminal) = &mut self.terminal {
591 let _ = terminal.enter_alt_screen().await;
592 }
593 // Intentionally do not continue; allow render below to redraw view
594 } else if msg.is::<crate::event::ExitAltScreenMsg>() {
595 if let Some(terminal) = &mut self.terminal {
596 let _ = terminal.exit_alt_screen().await;
597 }
598 // Intentionally do not continue; allow render below to redraw view
599 } else if msg.is::<crate::event::EveryMsgInternal>() {
600 // We need to consume the message to get ownership of the function
601 if let Ok(every_msg) = msg.downcast::<crate::event::EveryMsgInternal>() {
602 let duration = every_msg.duration;
603 let func = every_msg.func;
604 let cancellation_token = every_msg.cancellation_token.clone();
605 let timer_id = every_msg.timer_id;
606 let event_tx = self.event_tx.clone();
607
608 // Store the cancellation token for this timer
609 self.active_timers.insert(timer_id, cancellation_token.clone());
610
611 // Update memory monitoring
612 if let Some(ref monitor) = self.memory_monitor {
613 monitor.timer_added();
614 }
615
616 tokio::spawn(async move {
617 let mut ticker = tokio::time::interval(duration);
618 ticker.tick().await; // First tick completes immediately
619
620 loop {
621 tokio::select! {
622 _ = cancellation_token.cancelled() => {
623 // Timer was cancelled
624 break;
625 }
626 _ = ticker.tick() => {
627 let msg = func(duration);
628 if event_tx.send(msg).is_err() {
629 break; // Receiver dropped
630 }
631 }
632 }
633 }
634 });
635 continue; // Don't pass this to the model
636 }
637 } else if msg.is::<crate::event::BatchCmdMsg>() {
638 // Handle BatchCmdMsg: spawn all commands concurrently without waiting
639 if let Ok(batch_cmd_msg) = msg.downcast::<crate::event::BatchCmdMsg>() {
640 for c in batch_cmd_msg.0 {
641 let event_tx = self.event_tx.clone();
642 let shutdown_token = self.shutdown_token.clone();
643 if let Some(ref monitor) = self.memory_monitor {
644 monitor.task_spawned();
645 }
646 self.task_set.spawn(async move {
647 tokio::select! {
648 _ = shutdown_token.cancelled() => {
649 // Shutdown requested, don't process command
650 }
651 result = c => {
652 if let Some(msg) = result {
653 let _ = event_tx.send(msg);
654 }
655 }
656 }
657 });
658 }
659 }
660 continue; // We've handled the batch, don't pass it to the model
661 } else if msg.is::<crate::event::BatchMsgInternal>() {
662 if let Ok(batch_msg) = msg.downcast::<crate::event::BatchMsgInternal>() {
663 // Process each message in the batch and accumulate resulting cmds
664 let mut next_cmds: Vec<crate::command::Cmd> = Vec::new();
665 for batch_item in batch_msg.messages {
666 if batch_item.downcast_ref::<KillMsg>().is_some() {
667 // Immediate termination
668 break 'main_loop Err(Error::ProgramKilled);
669 }
670 if batch_item.downcast_ref::<QuitMsg>().is_some() {
671 should_quit = true;
672 }
673 if batch_item.downcast_ref::<crate::InterruptMsg>().is_some() {
674 should_interrupt = true;
675 }
676 if let Some(new_cmd) = model.update(batch_item) {
677 next_cmds.push(new_cmd);
678 }
679 }
680 if !next_cmds.is_empty() {
681 cmd = Some(crate::command::batch(next_cmds));
682 }
683 }
684 } else if msg.is::<crate::event::CancelTimerMsg>() {
685 if let Ok(cancel_msg) = msg.downcast::<crate::event::CancelTimerMsg>() {
686 if let Some(token) = self.active_timers.remove(&cancel_msg.timer_id) {
687 token.cancel();
688 // Update memory monitoring
689 if let Some(ref monitor) = self.memory_monitor {
690 monitor.timer_removed();
691 }
692 }
693 continue; // Don't pass this to the model
694 }
695 } else if msg.is::<crate::event::CancelAllTimersMsg>() {
696 // Cancel all active timers
697 let timer_count = self.active_timers.len();
698 for (_, token) in self.active_timers.drain() {
699 token.cancel();
700 }
701 // Update memory monitoring
702 if let Some(ref monitor) = self.memory_monitor {
703 for _ in 0..timer_count {
704 monitor.timer_removed();
705 }
706 }
707 continue; // Don't pass this to the model
708 } else if msg.is::<RequestWindowSizeMsg>() {
709 if let Some((width, height)) = self
710 .terminal
711 .as_ref()
712 .and_then(|terminal| terminal.size().ok())
713 {
714 let _ = self
715 .event_tx
716 .send(Box::new(WindowSizeMsg { width, height }) as Msg);
717 }
718 continue;
719 } else {
720 // Handle regular messages
721 let is_quit = msg.downcast_ref::<QuitMsg>().is_some();
722 let is_interrupt = msg.downcast_ref::<crate::InterruptMsg>().is_some();
723 cmd = model.update(msg);
724 if is_quit {
725 should_quit = true;
726 }
727 if is_interrupt {
728 should_interrupt = true;
729 }
730
731 // Update memory monitoring
732 if let Some(ref monitor) = self.memory_monitor {
733 monitor.message_processed();
734 }
735 }
736 if should_quit {
737 break Ok(model);
738 }
739 if should_interrupt {
740 break Err(Error::Interrupted);
741 }
742 if let Some(terminal) = &mut self.terminal {
743 let view = model.view();
744 terminal.render(&view).await?;
745 }
746 } else {
747 break Err(Error::ChannelReceive);
748 }
749 }
750 _ = async {
751 if self.config.signal_handler {
752 tokio::signal::ctrl_c().await.ok();
753 } else {
754 futures::future::pending::<()>().await;
755 }
756 }.fuse() => {
757 let _ = self.event_tx.send(Box::new(crate::InterruptMsg));
758 }
759 }
760 };
761
762 // Restore terminal state on exit
763 if let Some(terminal) = &mut self.terminal {
764 let _ = terminal.show_cursor().await;
765 let _ = terminal.disable_mouse().await;
766 let _ = terminal.disable_focus_reporting().await;
767 if self.config.alt_screen {
768 let _ = terminal.exit_alt_screen().await;
769 }
770 let _ = terminal.exit_raw_mode().await;
771 }
772
773 // Cleanup: cancel all tasks and wait for them to complete
774 self.cleanup_tasks().await;
775
776 result
777 }
778
779 /// Clean up all spawned tasks on program shutdown.
780 ///
781 /// This method is called internally during program shutdown to ensure
782 /// all background tasks are properly terminated. It:
783 /// 1. Cancels the shutdown token to signal all tasks to stop
784 /// 2. Cancels all active timers
785 /// 3. Waits for tasks to complete with a timeout
786 /// 4. Aborts any remaining unresponsive tasks
787 ///
788 /// This prevents resource leaks and ensures clean program termination.
789 async fn cleanup_tasks(&mut self) {
790 // Cancel the shutdown token to signal all tasks to stop
791 self.shutdown_token.cancel();
792
793 // Cancel all active timers
794 for (_, token) in self.active_timers.drain() {
795 token.cancel();
796 }
797
798 // Wait for all tasks to complete, with a timeout to avoid hanging
799 let timeout = std::time::Duration::from_millis(500);
800 let _ = tokio::time::timeout(timeout, async {
801 while (self.task_set.join_next().await).is_some() {
802 // Task completed
803 }
804 })
805 .await;
806
807 // Abort any remaining tasks that didn't respond to cancellation
808 self.task_set.abort_all();
809 }
810
811 /// Returns a sender that can be used to send messages to the `Program`'s event loop.
812 ///
813 /// This is useful for sending messages from outside the `Model`'s `update` method,
814 /// for example, from asynchronous tasks or other threads.
815 ///
816 /// # Returns
817 ///
818 /// An `EventSender` that can be used to send messages.
819 pub fn sender(&self) -> crate::event::EventSender {
820 self.event_tx.clone()
821 }
822
823 /// Sends a message to the `Program`'s event loop.
824 ///
825 /// This is a convenience method that wraps the `sender()` method.
826 /// The message will be processed by the model's `update` method.
827 ///
828 /// # Arguments
829 ///
830 /// * `msg` - The `Msg` to send to the event loop.
831 ///
832 /// # Returns
833 ///
834 /// A `Result` indicating success or a channel-related error if the message could not be sent.
835 ///
836 /// # Errors
837 ///
838 /// Returns an `Error` if:
839 /// - The event channel is full (for bounded channels)
840 /// - The receiver has been dropped
841 ///
842 /// # Example
843 ///
844 /// ```rust
845 /// # use bubbletea_rs::{Program, Model, KeyMsg};
846 /// # struct MyModel;
847 /// # impl Model for MyModel {
848 /// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (MyModel, None) }
849 /// # fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
850 /// # fn view(&self) -> String { String::new() }
851 /// # }
852 /// # async fn example() -> Result<(), bubbletea_rs::Error> {
853 /// let program = Program::<MyModel>::builder().build()?;
854 /// let key_msg = KeyMsg {
855 /// key: crossterm::event::KeyCode::Enter,
856 /// modifiers: crossterm::event::KeyModifiers::empty(),
857 /// };
858 /// program.send(Box::new(key_msg))?;
859 /// # Ok(())
860 /// # }
861 /// ```
862 pub fn send(&self, msg: Msg) -> Result<(), Error> {
863 self.event_tx.send(msg)
864 }
865
866 /// Sends a `QuitMsg` to the `Program`'s event loop, initiating a graceful shutdown.
867 ///
868 /// This causes the event loop to terminate gracefully after processing any
869 /// remaining messages in the queue. The terminal state will be properly restored.
870 ///
871 /// # Example
872 ///
873 /// ```rust
874 /// # use bubbletea_rs::{Program, Model};
875 /// # struct MyModel;
876 /// # impl Model for MyModel {
877 /// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (MyModel, None) }
878 /// # fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
879 /// # fn view(&self) -> String { String::new() }
880 /// # }
881 /// # async fn example() -> Result<(), bubbletea_rs::Error> {
882 /// let program = Program::<MyModel>::builder().build()?;
883 /// program.quit(); // Gracefully shutdown the program
884 /// # Ok(())
885 /// # }
886 /// ```
887 pub fn quit(&self) {
888 let _ = self.event_tx.send(Box::new(QuitMsg));
889 }
890
891 /// Get a reference to the memory monitor, if enabled.
892 ///
893 /// Returns `None` if memory monitoring is disabled.
894 pub fn memory_monitor(&self) -> Option<&crate::memory::MemoryMonitor> {
895 self.memory_monitor.as_ref()
896 }
897
898 /// Get memory usage health information, if monitoring is enabled.
899 ///
900 /// Returns `None` if memory monitoring is disabled.
901 pub fn memory_health(&self) -> Option<crate::memory::MemoryHealth> {
902 self.memory_monitor.as_ref().map(|m| m.check_health())
903 }
904
905 /// Sends a `KillMsg` to the `Program`'s event loop, initiating an immediate termination.
906 ///
907 /// Unlike `quit()`, which performs a graceful shutdown, `kill()` causes the event loop
908 /// to stop as soon as possible and returns `Error::ProgramKilled`.
909 pub fn kill(&self) {
910 let _ = self.event_tx.send(Box::new(KillMsg));
911 }
912
913 /// Waits for the `Program` to finish execution.
914 ///
915 /// This method blocks until the program's event loop has exited.
916 ///
917 /// # Note
918 ///
919 /// This is currently a no-op since the `Program` is consumed by `run()`.
920 /// In a real implementation, you'd need to track the program's state separately,
921 /// similar to how Go's context.Context works with goroutines.
922 ///
923 /// # Future Implementation
924 ///
925 /// A future version might track program state separately to enable proper
926 /// waiting functionality without consuming the `Program` instance.
927 pub async fn wait(&self) {
928 // Since the Program is consumed by run(), we can't really wait for it.
929 // This would need a different architecture to implement properly,
930 // similar to how Go's context.Context works with goroutines.
931 tokio::task::yield_now().await;
932 }
933
934 /// Releases control of the terminal.
935 ///
936 /// This method restores the terminal to its original state, disabling raw mode,
937 /// exiting alternate screen, disabling mouse and focus reporting, and showing the cursor.
938 pub async fn release_terminal(&mut self) -> Result<(), Error> {
939 if let Some(terminal) = &mut self.terminal {
940 terminal.exit_raw_mode().await?;
941 terminal.exit_alt_screen().await?;
942 terminal.disable_mouse().await?;
943 terminal.disable_focus_reporting().await?;
944 terminal.show_cursor().await?;
945 }
946 Ok(())
947 }
948
949 /// Restores control of the terminal.
950 ///
951 /// This method re-initializes the terminal based on the `ProgramConfig`,
952 /// enabling raw mode, entering alternate screen, enabling mouse and focus reporting,
953 /// and hiding the cursor.
954 pub async fn restore_terminal(&mut self) -> Result<(), Error> {
955 if let Some(terminal) = &mut self.terminal {
956 terminal.enter_raw_mode().await?;
957 if self.config.alt_screen {
958 terminal.enter_alt_screen().await?;
959 }
960 match self.config.mouse_motion {
961 MouseMotion::Cell => terminal.enable_mouse_cell_motion().await?,
962 MouseMotion::All => terminal.enable_mouse_all_motion().await?,
963 MouseMotion::None => (),
964 }
965 if self.config.report_focus {
966 terminal.enable_focus_reporting().await?;
967 }
968 if self.config.bracketed_paste {
969 terminal.enable_bracketed_paste().await?;
970 }
971 terminal.hide_cursor().await?;
972 }
973 Ok(())
974 }
975
976 /// Prints a line to the terminal without going through the renderer.
977 ///
978 /// This is useful for debugging or for outputting messages that shouldn't
979 /// be part of the managed UI. The output bypasses the normal rendering
980 /// pipeline and goes directly to stdout.
981 ///
982 /// # Arguments
983 ///
984 /// * `s` - The string to print, a newline will be automatically added.
985 ///
986 /// # Returns
987 ///
988 /// A `Result` indicating success or an IO error if printing fails.
989 ///
990 /// # Errors
991 ///
992 /// Returns an `Error` if stdout flushing fails.
993 ///
994 /// # Warning
995 ///
996 /// Using this method while the program is running may interfere with
997 /// the normal UI rendering. It's recommended to use this only for
998 /// debugging purposes or when the renderer is disabled.
999 pub async fn println(&mut self, s: String) -> Result<(), Error> {
1000 if let Some(_terminal) = &mut self.terminal {
1001 use std::io::Write;
1002 println!("{s}");
1003 std::io::stdout().flush()?;
1004 }
1005 Ok(())
1006 }
1007
1008 /// Prints formatted text to the terminal without going through the renderer.
1009 ///
1010 /// This is useful for debugging or for outputting messages that shouldn't
1011 /// be part of the managed UI. The output bypasses the normal rendering
1012 /// pipeline and goes directly to stdout without adding a newline.
1013 ///
1014 /// # Arguments
1015 ///
1016 /// * `s` - The string to print without adding a newline.
1017 ///
1018 /// # Returns
1019 ///
1020 /// A `Result` indicating success or an IO error if printing fails.
1021 ///
1022 /// # Errors
1023 ///
1024 /// Returns an `Error` if stdout flushing fails.
1025 ///
1026 /// # Warning
1027 ///
1028 /// Using this method while the program is running may interfere with
1029 /// the normal UI rendering. It's recommended to use this only for
1030 /// debugging purposes or when the renderer is disabled.
1031 pub async fn printf(&mut self, s: String) -> Result<(), Error> {
1032 if let Some(_terminal) = &mut self.terminal {
1033 use std::io::Write;
1034 print!("{s}");
1035 std::io::stdout().flush()?;
1036 }
1037 Ok(())
1038 }
1039}