Skip to main content

bubbletea/
message.rs

1//! Message types for the Elm Architecture.
2//!
3//! Messages are the only way to update the model in bubbletea. All user input,
4//! timer events, and custom events are represented as messages.
5
6use std::any::Any;
7use std::fmt;
8
9/// A type-erased message container.
10///
11/// Messages can be any type that is `Send + 'static`. Use [`Message::new`] to create
12/// a message and [`Message::downcast`] to retrieve the original type.
13///
14/// # Example
15///
16/// ```rust
17/// use bubbletea::Message;
18///
19/// struct MyMsg(i32);
20///
21/// let msg = Message::new(MyMsg(42));
22/// if let Some(my_msg) = msg.downcast::<MyMsg>() {
23///     assert_eq!(my_msg.0, 42);
24/// }
25/// ```
26pub struct Message(Box<dyn Any + Send>);
27
28impl Message {
29    /// Create a new message from any sendable type.
30    pub fn new<M: Any + Send + 'static>(msg: M) -> Self {
31        Self(Box::new(msg))
32    }
33
34    /// Try to downcast to a specific message type.
35    ///
36    /// Returns `Some(T)` if the message is of type `T`, otherwise `None`.
37    pub fn downcast<M: Any + Send + 'static>(self) -> Option<M> {
38        self.0.downcast::<M>().ok().map(|b| *b)
39    }
40
41    /// Try to get a reference to the message as a specific type.
42    pub fn downcast_ref<M: Any + Send + 'static>(&self) -> Option<&M> {
43        self.0.downcast_ref::<M>()
44    }
45
46    /// Check if the message is of a specific type.
47    pub fn is<M: Any + Send + 'static>(&self) -> bool {
48        self.0.is::<M>()
49    }
50}
51
52impl fmt::Debug for Message {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        f.debug_struct("Message").finish_non_exhaustive()
55    }
56}
57
58// Built-in message types
59
60/// Message to quit the program gracefully.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct QuitMsg;
63
64/// Message for Ctrl+C interrupt.
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct InterruptMsg;
67
68/// Message to suspend the program (Ctrl+Z).
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct SuspendMsg;
71
72/// Message when program resumes from suspension.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct ResumeMsg;
75
76/// Message containing terminal window size.
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub struct WindowSizeMsg {
79    /// Terminal width in columns.
80    pub width: u16,
81    /// Terminal height in rows.
82    pub height: u16,
83}
84
85/// Message when terminal gains focus.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct FocusMsg;
88
89/// Message when terminal loses focus.
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub struct BlurMsg;
92
93/// Internal message to set window title.
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub(crate) struct SetWindowTitleMsg(pub String);
96
97/// Internal message to request window size.
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub(crate) struct RequestWindowSizeMsg;
100
101/// Message for batch command execution.
102///
103/// This is produced by [`batch`](crate::batch) and handled by the program runtime.
104pub struct BatchMsg(pub Vec<super::Cmd>);
105
106/// Message for sequential command execution.
107///
108/// This is produced by [`sequence`](crate::sequence) and handled by the program runtime.
109pub struct SequenceMsg(pub Vec<super::Cmd>);
110
111/// Internal message for printing lines outside the TUI renderer.
112///
113/// This is produced by [`println`](crate::println) and [`printf`](crate::printf)
114/// and handled by the program runtime. Output is only written when not in
115/// alternate screen mode.
116#[derive(Debug, Clone, PartialEq, Eq)]
117pub(crate) struct PrintLineMsg(pub String);
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_message_downcast() {
125        struct TestMsg(i32);
126
127        let msg = Message::new(TestMsg(42));
128        assert!(msg.is::<TestMsg>());
129        let inner = msg.downcast::<TestMsg>().unwrap();
130        assert_eq!(inner.0, 42);
131    }
132
133    #[test]
134    fn test_message_downcast_wrong_type() {
135        struct TestMsg1;
136        struct TestMsg2;
137
138        let msg = Message::new(TestMsg1);
139        assert!(!msg.is::<TestMsg2>());
140        assert!(msg.downcast::<TestMsg2>().is_none());
141    }
142
143    #[test]
144    fn test_quit_msg() {
145        let msg = Message::new(QuitMsg);
146        assert!(msg.is::<QuitMsg>());
147    }
148
149    #[test]
150    fn test_window_size_msg() {
151        let msg = WindowSizeMsg {
152            width: 80,
153            height: 24,
154        };
155        assert_eq!(msg.width, 80);
156        assert_eq!(msg.height, 24);
157    }
158
159    // =========================================================================
160    // Comprehensive Message Tests (bd-1u1s)
161    // =========================================================================
162
163    #[test]
164    fn test_message_downcast_ref_success() {
165        struct TestMsg(i32);
166
167        let msg = Message::new(TestMsg(42));
168        // Use downcast_ref to borrow without consuming
169        let inner_ref = msg.downcast_ref::<TestMsg>().unwrap();
170        assert_eq!(inner_ref.0, 42);
171
172        // Can call downcast_ref multiple times
173        let inner_ref2 = msg.downcast_ref::<TestMsg>().unwrap();
174        assert_eq!(inner_ref2.0, 42);
175    }
176
177    #[test]
178    fn test_message_downcast_ref_wrong_type() {
179        struct TestMsg1(#[expect(dead_code)] i32);
180        struct TestMsg2;
181
182        let msg = Message::new(TestMsg1(42));
183        // downcast_ref to wrong type returns None
184        assert!(msg.downcast_ref::<TestMsg2>().is_none());
185    }
186
187    #[test]
188    fn test_message_is_without_consuming() {
189        struct TestMsg(i32);
190
191        let msg = Message::new(TestMsg(42));
192        // is<T>() doesn't consume the message
193        assert!(msg.is::<TestMsg>());
194        // Can still use the message after is<T>()
195        assert!(msg.is::<TestMsg>());
196        // And downcast it
197        assert_eq!(msg.downcast::<TestMsg>().unwrap().0, 42);
198    }
199
200    #[test]
201    fn test_message_debug_format() {
202        struct TestMsg;
203
204        let msg = Message::new(TestMsg);
205        let debug_str = format!("{:?}", msg);
206        // Debug should output something reasonable
207        assert!(debug_str.contains("Message"));
208    }
209
210    #[test]
211    fn test_interrupt_msg() {
212        let msg = Message::new(InterruptMsg);
213        assert!(msg.is::<InterruptMsg>());
214        // Verify it can be downcast
215        assert!(msg.downcast::<InterruptMsg>().is_some());
216    }
217
218    #[test]
219    fn test_suspend_msg() {
220        let msg = Message::new(SuspendMsg);
221        assert!(msg.is::<SuspendMsg>());
222    }
223
224    #[test]
225    fn test_resume_msg() {
226        let msg = Message::new(ResumeMsg);
227        assert!(msg.is::<ResumeMsg>());
228    }
229
230    #[test]
231    fn test_focus_msg() {
232        let msg = Message::new(FocusMsg);
233        assert!(msg.is::<FocusMsg>());
234    }
235
236    #[test]
237    fn test_blur_msg() {
238        let msg = Message::new(BlurMsg);
239        assert!(msg.is::<BlurMsg>());
240    }
241
242    #[test]
243    fn test_window_size_msg_in_message() {
244        let size = WindowSizeMsg {
245            width: 120,
246            height: 40,
247        };
248        let msg = Message::new(size);
249
250        assert!(msg.is::<WindowSizeMsg>());
251
252        let size_ref = msg.downcast_ref::<WindowSizeMsg>().unwrap();
253        assert_eq!(size_ref.width, 120);
254        assert_eq!(size_ref.height, 40);
255    }
256
257    #[test]
258    fn test_message_with_string() {
259        let msg = Message::new(String::from("hello"));
260        assert!(msg.is::<String>());
261        assert_eq!(msg.downcast::<String>().unwrap(), "hello");
262    }
263
264    #[test]
265    fn test_message_with_vec() {
266        let msg = Message::new(vec![1, 2, 3]);
267        assert!(msg.is::<Vec<i32>>());
268        assert_eq!(msg.downcast::<Vec<i32>>().unwrap(), vec![1, 2, 3]);
269    }
270
271    #[test]
272    fn test_message_with_tuple() {
273        let msg = Message::new((1i32, "hello", 2.71f64));
274        assert!(msg.is::<(i32, &str, f64)>());
275
276        let (a, b, c) = msg.downcast::<(i32, &str, f64)>().unwrap();
277        assert_eq!(a, 1);
278        assert_eq!(b, "hello");
279        assert!((c - 2.71).abs() < f64::EPSILON);
280    }
281
282    #[test]
283    fn test_message_with_unit() {
284        let msg = Message::new(());
285        assert!(msg.is::<()>());
286        assert!(msg.downcast::<()>().is_some());
287    }
288
289    #[test]
290    fn test_builtin_msg_equality() {
291        // Test PartialEq for built-in message types
292        assert_eq!(QuitMsg, QuitMsg);
293        assert_eq!(InterruptMsg, InterruptMsg);
294        assert_eq!(SuspendMsg, SuspendMsg);
295        assert_eq!(ResumeMsg, ResumeMsg);
296        assert_eq!(FocusMsg, FocusMsg);
297        assert_eq!(BlurMsg, BlurMsg);
298
299        let size1 = WindowSizeMsg {
300            width: 80,
301            height: 24,
302        };
303        let size2 = WindowSizeMsg {
304            width: 80,
305            height: 24,
306        };
307        let size3 = WindowSizeMsg {
308            width: 120,
309            height: 40,
310        };
311        assert_eq!(size1, size2);
312        assert_ne!(size1, size3);
313    }
314
315    #[test]
316    fn test_builtin_msg_clone() {
317        // Test Clone/Copy for built-in message types
318        let quit = QuitMsg;
319        let quit_copy = quit;
320        assert_eq!(quit, quit_copy);
321
322        let size = WindowSizeMsg {
323            width: 80,
324            height: 24,
325        };
326        let size_copy = size;
327        assert_eq!(size, size_copy);
328    }
329
330    #[test]
331    fn test_builtin_msg_copy() {
332        // Test Copy for built-in message types
333        let quit = QuitMsg;
334        let quit_copy = quit; // Copy, not move
335        assert_eq!(quit, quit_copy);
336
337        let size = WindowSizeMsg {
338            width: 80,
339            height: 24,
340        };
341        let size_copy = size; // Copy, not move
342        assert_eq!(size, size_copy);
343    }
344}