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
238 // Skip key_event.is_release() on Windows to prevent double keys
239 #[cfg(target_os = "windows")]
240 {
241 if key_event.is_press() {
242 if event_tx.send(Box::new(msg)).is_err() {
243 break;
244 }
245 }
246 }
247
248 #[cfg(not(target_os = "windows"))]
249 {
250 if event_tx.send(Box::new(msg)).is_err() {
251 break;
252 }
253 }
254 }
255 Ok(Event::Mouse(mouse_event)) => {
256 let msg = MouseMsg {
257 x: mouse_event.column,
258 y: mouse_event.row,
259 button: mouse_event.kind,
260 modifiers: mouse_event.modifiers,
261 };
262 if event_tx.send(Box::new(msg)).is_err() {
263 break;
264 }
265 }
266 Ok(Event::Resize(width, height)) => {
267 let msg = WindowSizeMsg { width, height };
268 if event_tx.send(Box::new(msg)).is_err() {
269 break;
270 }
271 }
272 Ok(Event::FocusGained) => {
273 let msg = crate::FocusMsg;
274 if event_tx.send(Box::new(msg)).is_err() {
275 break;
276 }
277 }
278 Ok(Event::FocusLost) => {
279 let msg = crate::BlurMsg;
280 if event_tx.send(Box::new(msg)).is_err() {
281 break;
282 }
283 }
284 Ok(Event::Paste(pasted_text)) => {
285 let msg = crate::event::PasteMsg(pasted_text);
286 if event_tx.send(Box::new(msg)).is_err() {
287 break;
288 }
289 }
290 Err(e) => {
291 return Err(Error::Io(e));
292 }
293 }
294 }
295
296 Ok(())
297 }
298
299 /// Runs the custom input handler from an async reader.
300 ///
301 /// This method reads line-based input from a custom async reader and converts
302 /// each line into individual `KeyMsg` events. Each character in a line becomes
303 /// a separate key event, and the newline is converted to an `Enter` key event.
304 ///
305 /// This is primarily intended for testing and scenarios where you need to
306 /// simulate keyboard input from a file or other source.
307 ///
308 /// # Arguments
309 ///
310 /// * `event_tx` - Channel sender for dispatching processed events
311 /// * `reader` - The async reader to read input from
312 ///
313 /// # Returns
314 ///
315 /// Returns `Ok(())` when EOF is reached or the event channel is closed,
316 /// or an `Error` if there's an I/O error reading from the source.
317 ///
318 /// # Errors
319 ///
320 /// Returns an error if there's an I/O error reading from the async reader.
321 ///
322 /// # Examples
323 ///
324 /// The input "hello\n" would generate the following key events:
325 /// - `KeyMsg { key: KeyCode::Char('h'), modifiers: KeyModifiers::NONE }`
326 /// - `KeyMsg { key: KeyCode::Char('e'), modifiers: KeyModifiers::NONE }`
327 /// - `KeyMsg { key: KeyCode::Char('l'), modifiers: KeyModifiers::NONE }`
328 /// - `KeyMsg { key: KeyCode::Char('l'), modifiers: KeyModifiers::NONE }`
329 /// - `KeyMsg { key: KeyCode::Char('o'), modifiers: KeyModifiers::NONE }`
330 /// - `KeyMsg { key: KeyCode::Enter, modifiers: KeyModifiers::NONE }`
331 async fn run_custom_input(
332 event_tx: crate::event::EventSender,
333 reader: Pin<Box<dyn AsyncRead + Send + Unpin>>,
334 ) -> Result<(), Error> {
335 let mut buf_reader = BufReader::new(reader);
336 let mut line = String::new();
337
338 loop {
339 line.clear();
340 match buf_reader.read_line(&mut line).await {
341 Ok(0) => break, // EOF
342 Ok(_) => {
343 // Process each character in the line as a separate key event
344 for ch in line.trim().chars() {
345 let msg = KeyMsg {
346 key: KeyCode::Char(ch),
347 modifiers: KeyModifiers::NONE,
348 };
349 if event_tx.send(Box::new(msg)).is_err() {
350 return Ok(());
351 }
352 }
353
354 // Send Enter key for the newline
355 if line.ends_with('\n') {
356 let msg = KeyMsg {
357 key: KeyCode::Enter,
358 modifiers: KeyModifiers::NONE,
359 };
360 if event_tx.send(Box::new(msg)).is_err() {
361 return Ok(());
362 }
363 }
364 }
365 Err(e) => return Err(Error::Io(e)),
366 }
367 }
368
369 Ok(())
370 }
371}