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}