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}