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
579                        // Handle special internal messages that need to consume the message
580                        if msg.is::<crate::event::ClearScreenMsg>() {
581                            if let Some(terminal) = &mut self.terminal {
582                                let _ = terminal.clear().await;
583                            }
584                            continue; // handled; don't pass to the model
585                        } else if msg.is::<crate::event::EnterAltScreenMsg>() {
586                            if let Some(terminal) = &mut self.terminal {
587                                let _ = terminal.enter_alt_screen().await;
588                            }
589                            // Intentionally do not continue; allow render below to redraw view
590                        } else if msg.is::<crate::event::ExitAltScreenMsg>() {
591                            if let Some(terminal) = &mut self.terminal {
592                                let _ = terminal.exit_alt_screen().await;
593                            }
594                            // Intentionally do not continue; allow render below to redraw view
595                        } else if msg.is::<crate::event::EveryMsgInternal>() {
596                            // We need to consume the message to get ownership of the function
597                            if let Ok(every_msg) = msg.downcast::<crate::event::EveryMsgInternal>() {
598                                let duration = every_msg.duration;
599                                let func = every_msg.func;
600                                let cancellation_token = every_msg.cancellation_token.clone();
601                                let timer_id = every_msg.timer_id;
602                                let event_tx = self.event_tx.clone();
603
604                                // Store the cancellation token for this timer
605                                self.active_timers.insert(timer_id, cancellation_token.clone());
606
607                                // Update memory monitoring
608                                if let Some(ref monitor) = self.memory_monitor {
609                                    monitor.timer_added();
610                                }
611
612                                tokio::spawn(async move {
613                                    let mut ticker = tokio::time::interval(duration);
614                                    ticker.tick().await; // First tick completes immediately
615
616                                    loop {
617                                        tokio::select! {
618                                            _ = cancellation_token.cancelled() => {
619                                                // Timer was cancelled
620                                                break;
621                                            }
622                                            _ = ticker.tick() => {
623                                                let msg = func(duration);
624                                                if event_tx.send(msg).is_err() {
625                                                    break; // Receiver dropped
626                                                }
627                                            }
628                                        }
629                                    }
630                                });
631                                continue; // Don't pass this to the model
632                            }
633                        } else if msg.is::<crate::event::BatchMsgInternal>() {
634                            if let Ok(batch_msg) = msg.downcast::<crate::event::BatchMsgInternal>() {
635                                // Process each message in the batch and accumulate resulting cmds
636                                let mut next_cmds: Vec<crate::command::Cmd> = Vec::new();
637                                for batch_item in batch_msg.messages {
638                                    if batch_item.downcast_ref::<KillMsg>().is_some() {
639                                        // Immediate termination
640                                        break 'main_loop Err(Error::ProgramKilled);
641                                    }
642                                    if batch_item.downcast_ref::<QuitMsg>().is_some() {
643                                        should_quit = true;
644                                    }
645                                    if let Some(new_cmd) = model.update(batch_item) {
646                                        next_cmds.push(new_cmd);
647                                    }
648                                }
649                                if !next_cmds.is_empty() {
650                                    cmd = Some(crate::command::batch(next_cmds));
651                                }
652                            }
653                        } else if msg.is::<crate::event::CancelTimerMsg>() {
654                            if let Ok(cancel_msg) = msg.downcast::<crate::event::CancelTimerMsg>() {
655                                if let Some(token) = self.active_timers.remove(&cancel_msg.timer_id) {
656                                    token.cancel();
657                                    // Update memory monitoring
658                                    if let Some(ref monitor) = self.memory_monitor {
659                                        monitor.timer_removed();
660                                    }
661                                }
662                                continue; // Don't pass this to the model
663                            }
664                        } else if msg.is::<crate::event::CancelAllTimersMsg>() {
665                            // Cancel all active timers
666                            let timer_count = self.active_timers.len();
667                            for (_, token) in self.active_timers.drain() {
668                                token.cancel();
669                            }
670                            // Update memory monitoring
671                            if let Some(ref monitor) = self.memory_monitor {
672                                for _ in 0..timer_count {
673                                    monitor.timer_removed();
674                                }
675                            }
676                            continue; // Don't pass this to the model
677                        } else {
678                            // Handle regular messages
679                            let is_quit = msg.downcast_ref::<QuitMsg>().is_some();
680                            cmd = model.update(msg);
681                            if is_quit {
682                                should_quit = true;
683                            }
684
685                            // Update memory monitoring
686                            if let Some(ref monitor) = self.memory_monitor {
687                                monitor.message_processed();
688                            }
689                        }
690                        if should_quit {
691                            break Ok(model);
692                        }
693                        if let Some(terminal) = &mut self.terminal {
694                            let view = model.view();
695                            terminal.render(&view).await?;
696                        }
697                    } else {
698                        break Err(Error::ChannelReceive);
699                    }
700                }
701                _ = async {
702                    if self.config.signal_handler {
703                        tokio::signal::ctrl_c().await.ok();
704                    } else {
705                        futures::future::pending::<()>().await;
706                    }
707                }.fuse() => {
708                    let _ = self.event_tx.send(Box::new(crate::InterruptMsg));
709                }
710            }
711        };
712
713        // Restore terminal state on exit
714        if let Some(terminal) = &mut self.terminal {
715            let _ = terminal.show_cursor().await;
716            let _ = terminal.disable_mouse().await;
717            let _ = terminal.disable_focus_reporting().await;
718            if self.config.alt_screen {
719                let _ = terminal.exit_alt_screen().await;
720            }
721            let _ = terminal.exit_raw_mode().await;
722        }
723
724        // Cleanup: cancel all tasks and wait for them to complete
725        self.cleanup_tasks().await;
726
727        result
728    }
729
730    /// Clean up all spawned tasks on program shutdown.
731    ///
732    /// This method is called internally during program shutdown to ensure
733    /// all background tasks are properly terminated. It:
734    /// 1. Cancels the shutdown token to signal all tasks to stop
735    /// 2. Cancels all active timers
736    /// 3. Waits for tasks to complete with a timeout
737    /// 4. Aborts any remaining unresponsive tasks
738    ///
739    /// This prevents resource leaks and ensures clean program termination.
740    async fn cleanup_tasks(&mut self) {
741        // Cancel the shutdown token to signal all tasks to stop
742        self.shutdown_token.cancel();
743
744        // Cancel all active timers
745        for (_, token) in self.active_timers.drain() {
746            token.cancel();
747        }
748
749        // Wait for all tasks to complete, with a timeout to avoid hanging
750        let timeout = std::time::Duration::from_millis(500);
751        let _ = tokio::time::timeout(timeout, async {
752            while (self.task_set.join_next().await).is_some() {
753                // Task completed
754            }
755        })
756        .await;
757
758        // Abort any remaining tasks that didn't respond to cancellation
759        self.task_set.abort_all();
760    }
761
762    /// Returns a sender that can be used to send messages to the `Program`'s event loop.
763    ///
764    /// This is useful for sending messages from outside the `Model`'s `update` method,
765    /// for example, from asynchronous tasks or other threads.
766    ///
767    /// # Returns
768    ///
769    /// An `EventSender` that can be used to send messages.
770    pub fn sender(&self) -> crate::event::EventSender {
771        self.event_tx.clone()
772    }
773
774    /// Sends a message to the `Program`'s event loop.
775    ///
776    /// This is a convenience method that wraps the `sender()` method.
777    /// The message will be processed by the model's `update` method.
778    ///
779    /// # Arguments
780    ///
781    /// * `msg` - The `Msg` to send to the event loop.
782    ///
783    /// # Returns
784    ///
785    /// A `Result` indicating success or a channel-related error if the message could not be sent.
786    ///
787    /// # Errors
788    ///
789    /// Returns an `Error` if:
790    /// - The event channel is full (for bounded channels)
791    /// - The receiver has been dropped
792    ///
793    /// # Example
794    ///
795    /// ```rust
796    /// # use bubbletea_rs::{Program, Model, KeyMsg};
797    /// # struct MyModel;
798    /// # impl Model for MyModel {
799    /// #     fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (MyModel, None) }
800    /// #     fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
801    /// #     fn view(&self) -> String { String::new() }
802    /// # }
803    /// # async fn example() -> Result<(), bubbletea_rs::Error> {
804    /// let program = Program::<MyModel>::builder().build()?;
805    /// let key_msg = KeyMsg {
806    ///     key: crossterm::event::KeyCode::Enter,
807    ///     modifiers: crossterm::event::KeyModifiers::empty(),
808    /// };
809    /// program.send(Box::new(key_msg))?;
810    /// # Ok(())
811    /// # }
812    /// ```
813    pub fn send(&self, msg: Msg) -> Result<(), Error> {
814        self.event_tx.send(msg)
815    }
816
817    /// Sends a `QuitMsg` to the `Program`'s event loop, initiating a graceful shutdown.
818    ///
819    /// This causes the event loop to terminate gracefully after processing any
820    /// remaining messages in the queue. The terminal state will be properly restored.
821    ///
822    /// # Example
823    ///
824    /// ```rust
825    /// # use bubbletea_rs::{Program, Model};
826    /// # struct MyModel;
827    /// # impl Model for MyModel {
828    /// #     fn init() -> (Self, Option<bubbletea_rs::Cmd>) { (MyModel, None) }
829    /// #     fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
830    /// #     fn view(&self) -> String { String::new() }
831    /// # }
832    /// # async fn example() -> Result<(), bubbletea_rs::Error> {
833    /// let program = Program::<MyModel>::builder().build()?;
834    /// program.quit(); // Gracefully shutdown the program
835    /// # Ok(())
836    /// # }
837    /// ```
838    pub fn quit(&self) {
839        let _ = self.event_tx.send(Box::new(QuitMsg));
840    }
841
842    /// Get a reference to the memory monitor, if enabled.
843    ///
844    /// Returns `None` if memory monitoring is disabled.
845    pub fn memory_monitor(&self) -> Option<&crate::memory::MemoryMonitor> {
846        self.memory_monitor.as_ref()
847    }
848
849    /// Get memory usage health information, if monitoring is enabled.
850    ///
851    /// Returns `None` if memory monitoring is disabled.
852    pub fn memory_health(&self) -> Option<crate::memory::MemoryHealth> {
853        self.memory_monitor.as_ref().map(|m| m.check_health())
854    }
855
856    /// Sends a `KillMsg` to the `Program`'s event loop, initiating an immediate termination.
857    ///
858    /// Unlike `quit()`, which performs a graceful shutdown, `kill()` causes the event loop
859    /// to stop as soon as possible and returns `Error::ProgramKilled`.
860    pub fn kill(&self) {
861        let _ = self.event_tx.send(Box::new(KillMsg));
862    }
863
864    /// Waits for the `Program` to finish execution.
865    ///
866    /// This method blocks until the program's event loop has exited.
867    ///
868    /// # Note
869    ///
870    /// This is currently a no-op since the `Program` is consumed by `run()`.
871    /// In a real implementation, you'd need to track the program's state separately,
872    /// similar to how Go's context.Context works with goroutines.
873    ///
874    /// # Future Implementation
875    ///
876    /// A future version might track program state separately to enable proper
877    /// waiting functionality without consuming the `Program` instance.
878    pub async fn wait(&self) {
879        // Since the Program is consumed by run(), we can't really wait for it.
880        // This would need a different architecture to implement properly,
881        // similar to how Go's context.Context works with goroutines.
882        tokio::task::yield_now().await;
883    }
884
885    /// Releases control of the terminal.
886    ///
887    /// This method restores the terminal to its original state, disabling raw mode,
888    /// exiting alternate screen, disabling mouse and focus reporting, and showing the cursor.
889    pub async fn release_terminal(&mut self) -> Result<(), Error> {
890        if let Some(terminal) = &mut self.terminal {
891            terminal.exit_raw_mode().await?;
892            terminal.exit_alt_screen().await?;
893            terminal.disable_mouse().await?;
894            terminal.disable_focus_reporting().await?;
895            terminal.show_cursor().await?;
896        }
897        Ok(())
898    }
899
900    /// Restores control of the terminal.
901    ///
902    /// This method re-initializes the terminal based on the `ProgramConfig`,
903    /// enabling raw mode, entering alternate screen, enabling mouse and focus reporting,
904    /// and hiding the cursor.
905    pub async fn restore_terminal(&mut self) -> Result<(), Error> {
906        if let Some(terminal) = &mut self.terminal {
907            terminal.enter_raw_mode().await?;
908            if self.config.alt_screen {
909                terminal.enter_alt_screen().await?;
910            }
911            match self.config.mouse_motion {
912                MouseMotion::Cell => terminal.enable_mouse_cell_motion().await?,
913                MouseMotion::All => terminal.enable_mouse_all_motion().await?,
914                MouseMotion::None => (),
915            }
916            if self.config.report_focus {
917                terminal.enable_focus_reporting().await?;
918            }
919            if self.config.bracketed_paste {
920                terminal.enable_bracketed_paste().await?;
921            }
922            terminal.hide_cursor().await?;
923        }
924        Ok(())
925    }
926
927    /// Prints a line to the terminal without going through the renderer.
928    ///
929    /// This is useful for debugging or for outputting messages that shouldn't
930    /// be part of the managed UI. The output bypasses the normal rendering
931    /// pipeline and goes directly to stdout.
932    ///
933    /// # Arguments
934    ///
935    /// * `s` - The string to print, a newline will be automatically added.
936    ///
937    /// # Returns
938    ///
939    /// A `Result` indicating success or an IO error if printing fails.
940    ///
941    /// # Errors
942    ///
943    /// Returns an `Error` if stdout flushing fails.
944    ///
945    /// # Warning
946    ///
947    /// Using this method while the program is running may interfere with
948    /// the normal UI rendering. It's recommended to use this only for
949    /// debugging purposes or when the renderer is disabled.
950    pub async fn println(&mut self, s: String) -> Result<(), Error> {
951        if let Some(_terminal) = &mut self.terminal {
952            use std::io::Write;
953            println!("{s}");
954            std::io::stdout().flush()?;
955        }
956        Ok(())
957    }
958
959    /// Prints formatted text to the terminal without going through the renderer.
960    ///
961    /// This is useful for debugging or for outputting messages that shouldn't
962    /// be part of the managed UI. The output bypasses the normal rendering
963    /// pipeline and goes directly to stdout without adding a newline.
964    ///
965    /// # Arguments
966    ///
967    /// * `s` - The string to print without adding a newline.
968    ///
969    /// # Returns
970    ///
971    /// A `Result` indicating success or an IO error if printing fails.
972    ///
973    /// # Errors
974    ///
975    /// Returns an `Error` if stdout flushing fails.
976    ///
977    /// # Warning
978    ///
979    /// Using this method while the program is running may interfere with
980    /// the normal UI rendering. It's recommended to use this only for
981    /// debugging purposes or when the renderer is disabled.
982    pub async fn printf(&mut self, s: String) -> Result<(), Error> {
983        if let Some(_terminal) = &mut self.terminal {
984            use std::io::Write;
985            print!("{s}");
986            std::io::stdout().flush()?;
987        }
988        Ok(())
989    }
990}