bubbletea_rs/
input.rs

1//! Input handling system for the Bubble Tea TUI framework.
2//!
3//! This module provides the core input processing functionality for `bubbletea-rs`.
4//! It is responsible for reading terminal events (keyboard, mouse, resize, focus, paste)
5//! and converting them into messages that can be processed by the application's model
6//! following the Model-View-Update (MVU) pattern.
7//!
8//! # Key Components
9//!
10//! - [`InputHandler`] - The main event processor that runs the input loop
11//! - [`InputSource`] - Enum defining different input sources (terminal or custom)
12//!
13//! # Examples
14//!
15//! Basic usage with terminal input:
16//!
17//! ```rust
18//! use bubbletea_rs::input::{InputHandler, InputSource};
19//! use tokio::sync::mpsc;
20//!
21//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
22//! let (tx, rx) = mpsc::unbounded_channel();
23//! let input_handler = InputHandler::new(tx);
24//!
25//! // Start the input processing loop
26//! tokio::spawn(async move {
27//!     input_handler.run().await
28//! });
29//! # Ok(())
30//! # }
31//! ```
32//!
33//! Using a custom input source:
34//!
35//! ```rust
36//! use bubbletea_rs::input::{InputHandler, InputSource};
37//! use tokio::sync::mpsc;
38//! use std::pin::Pin;
39//!
40//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
41//! let (tx, rx) = mpsc::unbounded_channel();
42//! let custom_reader = Box::pin(std::io::Cursor::new("hello\n"));
43//! let input_source = InputSource::Custom(custom_reader);
44//! let input_handler = InputHandler::with_source(tx, input_source);
45//!
46//! // Process input from the custom source
47//! input_handler.run().await?;
48//! # Ok(())
49//! # }
50//! ```
51
52use crate::{Error, KeyMsg, MouseMsg, WindowSizeMsg};
53use crossterm::event::{Event, EventStream, KeyCode, KeyModifiers};
54use futures::StreamExt;
55use std::pin::Pin;
56use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
57
58/// Represents different input sources that the `InputHandler` can read from.
59///
60/// This enum allows the program to read input from either the standard crossterm
61/// event stream (for regular terminal input) or from a custom async reader.
62pub enum InputSource {
63    /// Standard terminal input using crossterm's event stream.
64    /// This is the default and handles keyboard, mouse, and resize events.
65    Terminal,
66
67    /// Custom input reader that implements `AsyncRead + Send + Unpin`.
68    /// This allows reading input from files, network streams, or other sources.
69    /// The custom reader is expected to provide line-based input.
70    Custom(Pin<Box<dyn AsyncRead + Send + Unpin>>),
71}
72
73/// `InputHandler` is responsible for processing terminal events and sending them
74/// as messages to the `Program`'s event loop.
75///
76/// It continuously reads events from the `crossterm` event stream and converts
77/// them into appropriate `Msg` types.
78pub struct InputHandler {
79    /// The sender half of an MPSC channel used to send messages
80    /// to the `Program`'s event loop.
81    pub event_tx: crate::event::EventSender,
82
83    /// The input source to read from.
84    pub input_source: InputSource,
85}
86
87impl InputHandler {
88    /// Creates a new `InputHandler` with the given message sender using terminal input.
89    ///
90    /// This constructor sets up the input handler to read from the standard terminal
91    /// using crossterm's event stream. This is the most common usage pattern.
92    ///
93    /// # Arguments
94    ///
95    /// * `event_tx` - An `EventSender` to send processed events to the main program loop
96    ///
97    /// # Examples
98    ///
99    /// ```rust
100    /// use bubbletea_rs::input::InputHandler;
101    /// use tokio::sync::mpsc;
102    ///
103    /// let (tx, rx) = mpsc::unbounded_channel();
104    /// let input_handler = InputHandler::new(tx);
105    /// ```
106    pub fn new<T>(event_tx: T) -> Self
107    where
108        T: Into<crate::event::EventSender>,
109    {
110        Self {
111            event_tx: event_tx.into(),
112            input_source: InputSource::Terminal,
113        }
114    }
115
116    /// Creates a new `InputHandler` with a custom input source.
117    ///
118    /// This constructor allows you to specify a custom input source instead of
119    /// the default terminal input. This is useful for testing, reading from files,
120    /// or processing input from network streams.
121    ///
122    /// # Arguments
123    ///
124    /// * `event_tx` - An `EventSender` to send processed events to the main program loop
125    /// * `input_source` - The `InputSource` to read from (terminal or custom reader)
126    ///
127    /// # Examples
128    ///
129    /// Reading from a file:
130    ///
131    /// ```rust
132    /// use bubbletea_rs::input::{InputHandler, InputSource};
133    /// use tokio::sync::mpsc;
134    /// use std::pin::Pin;
135    ///
136    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
137    /// let (tx, rx) = mpsc::unbounded_channel();
138    /// let file_content = std::io::Cursor::new("test input\n");
139    /// let custom_source = InputSource::Custom(Box::pin(file_content));
140    /// let input_handler = InputHandler::with_source(tx, custom_source);
141    /// # Ok(())
142    /// # }
143    /// ```
144    pub fn with_source<T>(event_tx: T, input_source: InputSource) -> Self
145    where
146        T: Into<crate::event::EventSender>,
147    {
148        Self {
149            event_tx: event_tx.into(),
150            input_source,
151        }
152    }
153
154    /// Runs the input handler loop asynchronously.
155    ///
156    /// This method continuously reads events from the configured input source
157    /// and processes them until the loop terminates. It converts raw terminal
158    /// events into typed `Msg` objects and sends them through the event channel
159    /// to the main program loop.
160    ///
161    /// The loop terminates when:
162    /// - The event sender channel is closed (receiver dropped)
163    /// - An I/O error occurs while reading input
164    /// - EOF is reached for custom input sources
165    ///
166    /// # Returns
167    ///
168    /// Returns `Ok(())` on normal termination, or an `Error` if an I/O error occurs.
169    ///
170    /// # Errors
171    ///
172    /// This function returns an error if:
173    /// - There's an I/O error reading from the input source
174    /// - The underlying crossterm event stream encounters an error
175    ///
176    /// # Examples
177    ///
178    /// ```rust
179    /// use bubbletea_rs::input::InputHandler;
180    /// use tokio::sync::mpsc;
181    ///
182    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
183    /// let (tx, mut rx) = mpsc::unbounded_channel();
184    /// let input_handler = InputHandler::new(tx);
185    ///
186    /// // Run the input handler in a separate task
187    /// let input_task = tokio::spawn(async move {
188    ///     input_handler.run().await
189    /// });
190    ///
191    /// // Process incoming messages
192    /// while let Some(msg) = rx.recv().await {
193    ///     // Handle the message...
194    /// }
195    /// # Ok(())
196    /// # }
197    /// ```
198    pub async fn run(self) -> Result<(), Error> {
199        let event_tx = self.event_tx;
200        match self.input_source {
201            InputSource::Terminal => Self::run_terminal_input(event_tx).await,
202            InputSource::Custom(reader) => Self::run_custom_input(event_tx, reader).await,
203        }
204    }
205
206    /// Runs the terminal input handler using crossterm's event stream.
207    ///
208    /// This method processes standard terminal events including:
209    /// - Keyboard input (keys and modifiers)
210    /// - Mouse events (clicks, movements, scrolling)
211    /// - Terminal resize events
212    /// - Focus gained/lost events
213    /// - Paste events (when bracketed paste is enabled)
214    ///
215    /// # Arguments
216    ///
217    /// * `event_tx` - Channel sender for dispatching processed events
218    ///
219    /// # Returns
220    ///
221    /// Returns `Ok(())` when the event stream ends normally, or an `Error`
222    /// if there's an I/O error reading from the terminal.
223    ///
224    /// # Errors
225    ///
226    /// Returns an error if crossterm's event stream encounters an I/O error.
227    async fn run_terminal_input(event_tx: crate::event::EventSender) -> Result<(), Error> {
228        let mut event_stream = EventStream::new();
229
230        while let Some(event) = event_stream.next().await {
231            match event {
232                Ok(Event::Key(key_event)) => {
233                    let msg = KeyMsg {
234                        key: key_event.code,
235                        modifiers: key_event.modifiers,
236                    };
237                    if event_tx.send(Box::new(msg)).is_err() {
238                        break;
239                    }
240                }
241                Ok(Event::Mouse(mouse_event)) => {
242                    let msg = MouseMsg {
243                        x: mouse_event.column,
244                        y: mouse_event.row,
245                        button: mouse_event.kind,
246                        modifiers: mouse_event.modifiers,
247                    };
248                    if event_tx.send(Box::new(msg)).is_err() {
249                        break;
250                    }
251                }
252                Ok(Event::Resize(width, height)) => {
253                    let msg = WindowSizeMsg { width, height };
254                    if event_tx.send(Box::new(msg)).is_err() {
255                        break;
256                    }
257                }
258                Ok(Event::FocusGained) => {
259                    let msg = crate::FocusMsg;
260                    if event_tx.send(Box::new(msg)).is_err() {
261                        break;
262                    }
263                }
264                Ok(Event::FocusLost) => {
265                    let msg = crate::BlurMsg;
266                    if event_tx.send(Box::new(msg)).is_err() {
267                        break;
268                    }
269                }
270                Ok(Event::Paste(pasted_text)) => {
271                    let msg = crate::event::PasteMsg(pasted_text);
272                    if event_tx.send(Box::new(msg)).is_err() {
273                        break;
274                    }
275                }
276                Err(e) => {
277                    return Err(Error::Io(e));
278                }
279            }
280        }
281
282        Ok(())
283    }
284
285    /// Runs the custom input handler from an async reader.
286    ///
287    /// This method reads line-based input from a custom async reader and converts
288    /// each line into individual `KeyMsg` events. Each character in a line becomes
289    /// a separate key event, and the newline is converted to an `Enter` key event.
290    ///
291    /// This is primarily intended for testing and scenarios where you need to
292    /// simulate keyboard input from a file or other source.
293    ///
294    /// # Arguments
295    ///
296    /// * `event_tx` - Channel sender for dispatching processed events
297    /// * `reader` - The async reader to read input from
298    ///
299    /// # Returns
300    ///
301    /// Returns `Ok(())` when EOF is reached or the event channel is closed,
302    /// or an `Error` if there's an I/O error reading from the source.
303    ///
304    /// # Errors
305    ///
306    /// Returns an error if there's an I/O error reading from the async reader.
307    ///
308    /// # Examples
309    ///
310    /// The input "hello\n" would generate the following key events:
311    /// - `KeyMsg { key: KeyCode::Char('h'), modifiers: KeyModifiers::NONE }`
312    /// - `KeyMsg { key: KeyCode::Char('e'), modifiers: KeyModifiers::NONE }`
313    /// - `KeyMsg { key: KeyCode::Char('l'), modifiers: KeyModifiers::NONE }`
314    /// - `KeyMsg { key: KeyCode::Char('l'), modifiers: KeyModifiers::NONE }`
315    /// - `KeyMsg { key: KeyCode::Char('o'), modifiers: KeyModifiers::NONE }`
316    /// - `KeyMsg { key: KeyCode::Enter, modifiers: KeyModifiers::NONE }`
317    async fn run_custom_input(
318        event_tx: crate::event::EventSender,
319        reader: Pin<Box<dyn AsyncRead + Send + Unpin>>,
320    ) -> Result<(), Error> {
321        let mut buf_reader = BufReader::new(reader);
322        let mut line = String::new();
323
324        loop {
325            line.clear();
326            match buf_reader.read_line(&mut line).await {
327                Ok(0) => break, // EOF
328                Ok(_) => {
329                    // Process each character in the line as a separate key event
330                    for ch in line.trim().chars() {
331                        let msg = KeyMsg {
332                            key: KeyCode::Char(ch),
333                            modifiers: KeyModifiers::NONE,
334                        };
335                        if event_tx.send(Box::new(msg)).is_err() {
336                            return Ok(());
337                        }
338                    }
339
340                    // Send Enter key for the newline
341                    if line.ends_with('\n') {
342                        let msg = KeyMsg {
343                            key: KeyCode::Enter,
344                            modifiers: KeyModifiers::NONE,
345                        };
346                        if event_tx.send(Box::new(msg)).is_err() {
347                            return Ok(());
348                        }
349                    }
350                }
351                Err(e) => return Err(Error::Io(e)),
352            }
353        }
354
355        Ok(())
356    }
357}