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