bubbletea_rs/
terminal.rs

1//! Terminal management and abstraction for bubbletea-rs.
2//!
3//! This module provides terminal interfaces and implementations for managing
4//! terminal state, input/output operations, and feature toggling. It includes
5//! both a full-featured terminal implementation using crossterm and a dummy
6//! implementation for testing purposes.
7//!
8//! # Key Components
9//!
10//! - [`TerminalInterface`]: Trait defining terminal operations
11//! - [`Terminal`]: Full crossterm-based terminal implementation
12//! - [`DummyTerminal`]: No-op terminal for testing
13//!
14//! # Features
15//!
16//! - Raw mode management for capturing all key events
17//! - Alternate screen buffer support
18//! - Mouse event capture with different motion reporting levels
19//! - Focus change reporting
20//! - Bracketed paste mode for distinguishing pasted vs typed text
21//! - Cursor visibility control
22//! - Efficient rendering with buffering
23
24use crate::Error;
25use crossterm::{
26    cursor::{Hide, Show},
27    event::{
28        DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
29        EnableFocusChange, EnableMouseCapture,
30    },
31    execute,
32    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
33};
34use std::io::{self, Write};
35use std::sync::Arc;
36use tokio::io::AsyncWrite;
37use tokio::sync::Mutex;
38
39/// A trait for abstracting terminal operations.
40///
41/// This trait provides a unified interface for terminal management across
42/// different implementations. It supports both direct terminal access and
43/// custom output writers for testing or alternative output destinations.
44///
45/// # Design Philosophy
46///
47/// All methods are designed to be idempotent - calling them multiple times
48/// with the same parameters should be safe and efficient. Implementations
49/// should track state to avoid unnecessary system calls.
50///
51/// # Example
52///
53/// ```rust
54/// use bubbletea_rs::terminal::{TerminalInterface, Terminal};
55/// use bubbletea_rs::Error;
56///
57/// # async fn example() -> Result<(), Error> {
58/// let mut terminal = Terminal::new(None)?;
59/// terminal.enter_raw_mode().await?;
60/// terminal.hide_cursor().await?;
61/// terminal.render("Hello, world!").await?;
62/// terminal.show_cursor().await?;
63/// terminal.exit_raw_mode().await?;
64/// # Ok(())
65/// # }
66/// ```
67#[async_trait::async_trait]
68pub trait TerminalInterface {
69    /// Construct a new terminal implementation.
70    ///
71    /// Accepts an optional asynchronous output writer. When provided, rendering
72    /// will write to this writer instead of stdout. This is useful for testing
73    /// or redirecting output to files, network streams, or other destinations.
74    ///
75    /// # Arguments
76    ///
77    /// * `output_writer` - Optional custom output destination. If `None`, uses stdout.
78    ///
79    /// # Returns
80    ///
81    /// A new terminal implementation instance.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error if terminal initialization fails or if the output writer
86    /// cannot be set up properly.
87    fn new(output_writer: Option<Arc<Mutex<dyn AsyncWrite + Send + Unpin>>>) -> Result<Self, Error>
88    where
89        Self: Sized;
90    /// Enable raw mode (disables canonical input processing).
91    ///
92    /// Raw mode allows the application to receive all key events immediately
93    /// without line buffering or special character processing. This is essential
94    /// for interactive TUI applications.
95    ///
96    /// # Effects
97    ///
98    /// - Disables line buffering (canonical mode)
99    /// - Disables echo of typed characters
100    /// - Enables immediate key event delivery
101    /// - Disables special character processing (Ctrl+C, Ctrl+Z, etc.)
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the terminal cannot be switched to raw mode.
106    async fn enter_raw_mode(&mut self) -> Result<(), Error>;
107    /// Disable raw mode and restore canonical input processing.
108    ///
109    /// Restores the terminal to its original state with line buffering,
110    /// echo, and special character processing enabled.
111    ///
112    /// # Effects
113    ///
114    /// - Re-enables line buffering (canonical mode)
115    /// - Re-enables echo of typed characters
116    /// - Restores special character processing
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if the terminal cannot be restored to canonical mode.
121    async fn exit_raw_mode(&mut self) -> Result<(), Error>;
122    /// Enter the alternate screen buffer.
123    ///
124    /// Switches to an alternate screen buffer, preserving the current terminal
125    /// content. This allows the application to run in a "clean" screen that
126    /// can be restored when the application exits.
127    ///
128    /// # Effects
129    ///
130    /// - Saves current screen content
131    /// - Switches to alternate buffer
132    /// - Clears the alternate buffer
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if the terminal doesn't support alternate screens
137    /// or if the switch fails.
138    async fn enter_alt_screen(&mut self) -> Result<(), Error>;
139    /// Leave the alternate screen buffer.
140    ///
141    /// Switches back to the main screen buffer, restoring the original
142    /// terminal content that was visible before entering alternate screen.
143    ///
144    /// # Effects
145    ///
146    /// - Restores original screen content
147    /// - Switches back to main buffer
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if the switch back to main screen fails.
152    async fn exit_alt_screen(&mut self) -> Result<(), Error>;
153    /// Enable basic mouse capture.
154    ///
155    /// Enables the terminal to capture mouse events including clicks,
156    /// releases, and basic movement. The application will receive mouse
157    /// events through the normal input stream.
158    ///
159    /// # Errors
160    ///
161    /// Returns an error if mouse capture cannot be enabled.
162    async fn enable_mouse(&mut self) -> Result<(), Error>;
163    /// Enable cell-motion mouse reporting.
164    ///
165    /// Enables mouse motion reporting when the mouse moves between different
166    /// character cells. This provides more detailed movement tracking than
167    /// basic mouse capture while being less intensive than all-motion reporting.
168    ///
169    /// # Errors
170    ///
171    /// Returns an error if cell-motion mouse reporting cannot be enabled.
172    async fn enable_mouse_cell_motion(&mut self) -> Result<(), Error>;
173    /// Enable high-resolution mouse reporting.
174    ///
175    /// Enables reporting of all mouse movement, including sub-cell movements.
176    /// This provides the highest fidelity mouse tracking but generates many
177    /// more events and should be used carefully to avoid overwhelming the
178    /// application.
179    ///
180    /// # Performance Note
181    ///
182    /// This mode generates significantly more events than other mouse modes.
183    /// Use only when precise mouse tracking is required.
184    ///
185    /// # Errors
186    ///
187    /// Returns an error if all-motion mouse reporting cannot be enabled.
188    async fn enable_mouse_all_motion(&mut self) -> Result<(), Error>;
189    /// Disable all mouse capture modes.
190    ///
191    /// Disables mouse event capture and reporting. After calling this,
192    /// the terminal will not send mouse events to the application.
193    ///
194    /// # Errors
195    ///
196    /// Returns an error if mouse capture cannot be disabled.
197    async fn disable_mouse(&mut self) -> Result<(), Error>;
198    /// Enable terminal focus change reporting.
199    ///
200    /// Enables the terminal to report when it gains or loses focus.
201    /// The application will receive focus/blur events when the terminal
202    /// window becomes active or inactive.
203    ///
204    /// # Use Cases
205    ///
206    /// - Pausing animations when the terminal loses focus
207    /// - Changing display intensity or colors
208    /// - Triggering auto-save when focus is lost
209    ///
210    /// # Errors
211    ///
212    /// Returns an error if focus reporting cannot be enabled.
213    async fn enable_focus_reporting(&mut self) -> Result<(), Error>;
214    /// Disable terminal focus change reporting.
215    ///
216    /// Disables focus change event reporting. The terminal will no longer
217    /// send focus/blur events to the application.
218    ///
219    /// # Errors
220    ///
221    /// Returns an error if focus reporting cannot be disabled.
222    async fn disable_focus_reporting(&mut self) -> Result<(), Error>;
223    /// Enable bracketed paste mode.
224    ///
225    /// Enables bracketed paste mode, which wraps pasted text in special
226    /// escape sequences. This allows the application to distinguish between
227    /// text that was typed character-by-character and text that was pasted
228    /// as a block.
229    ///
230    /// # Benefits
231    ///
232    /// - Prevents auto-indentation from corrupting pasted code
233    /// - Allows special handling of large text blocks
234    /// - Improves security by identifying untrusted input
235    ///
236    /// # Errors
237    ///
238    /// Returns an error if bracketed paste mode cannot be enabled.
239    async fn enable_bracketed_paste(&mut self) -> Result<(), Error>;
240    /// Disable bracketed paste mode.
241    ///
242    /// Disables bracketed paste mode, returning to normal paste behavior
243    /// where pasted text is indistinguishable from typed text.
244    ///
245    /// # Errors
246    ///
247    /// Returns an error if bracketed paste mode cannot be disabled.
248    async fn disable_bracketed_paste(&mut self) -> Result<(), Error>;
249    /// Show the cursor if hidden.
250    ///
251    /// Makes the cursor visible if it was previously hidden. This is typically
252    /// called when exiting the application or when cursor visibility is needed
253    /// for user input.
254    ///
255    /// # Errors
256    ///
257    /// Returns an error if the cursor visibility cannot be changed.
258    async fn show_cursor(&mut self) -> Result<(), Error>;
259    /// Hide the cursor if visible.
260    ///
261    /// Hides the cursor from view. This is commonly done in TUI applications
262    /// to prevent the cursor from interfering with the visual layout or
263    /// to create a cleaner appearance.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if the cursor visibility cannot be changed.
268    async fn hide_cursor(&mut self) -> Result<(), Error>;
269    /// Clear the visible screen contents.
270    ///
271    /// Clears the entire visible screen, typically filling it with the
272    /// default background color. The cursor position may be reset to
273    /// the top-left corner.
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if the screen cannot be cleared.
278    async fn clear(&mut self) -> Result<(), Error>;
279    /// Render the provided content to the terminal.
280    ///
281    /// Displays the given content on the terminal screen. This typically
282    /// involves clearing the screen and writing the new content from the
283    /// top-left corner. Newlines in the content will be properly handled
284    /// for the target terminal.
285    ///
286    /// # Arguments
287    ///
288    /// * `content` - The text content to display. May contain ANSI escape
289    ///   sequences for colors and formatting.
290    ///
291    /// # Performance
292    ///
293    /// Implementations should buffer output efficiently to minimize the
294    /// number of system calls and reduce flicker.
295    ///
296    /// # Errors
297    ///
298    /// Returns an error if the content cannot be written to the terminal
299    /// or output writer.
300    async fn render(&mut self, content: &str) -> Result<(), Error>;
301    /// Get the current terminal size as (columns, rows).
302    ///
303    /// Returns the current dimensions of the terminal in character cells.
304    /// This information is useful for layout calculations and ensuring
305    /// content fits within the visible area.
306    ///
307    /// # Returns
308    ///
309    /// A tuple of `(width, height)` where:
310    /// - `width` is the number of character columns
311    /// - `height` is the number of character rows
312    ///
313    /// # Errors
314    ///
315    /// Returns an error if the terminal size cannot be determined.
316    ///
317    /// # Note
318    ///
319    /// Terminal size can change during program execution due to window
320    /// resizing. Applications should handle size change events appropriately.
321    fn size(&self) -> Result<(u16, u16), Error>;
322}
323
324/// Terminal state manager using crossterm for actual terminal control.
325///
326/// This is the primary terminal implementation that provides full terminal
327/// control capabilities through the crossterm library. It maintains state
328/// to ensure operations are idempotent and efficient.
329///
330/// # State Tracking
331///
332/// The terminal tracks various state flags to avoid unnecessary operations:
333/// - Raw mode status
334/// - Alternate screen status
335/// - Mouse capture status
336/// - Focus reporting status
337/// - Cursor visibility
338///
339/// # Performance
340///
341/// - Uses a pre-allocated render buffer to minimize allocations
342/// - Tracks state to avoid redundant terminal operations
343/// - Efficiently handles newline conversion for cross-platform compatibility
344///
345/// # Example
346///
347/// ```rust
348/// use bubbletea_rs::terminal::{Terminal, TerminalInterface};
349/// use bubbletea_rs::Error;
350///
351/// # async fn example() -> Result<(), Error> {
352/// let mut terminal = Terminal::new(None)?;
353///
354/// // Set up terminal for TUI mode
355/// terminal.enter_raw_mode().await?;
356/// terminal.enter_alt_screen().await?;
357/// terminal.hide_cursor().await?;
358///
359/// // Render some content
360/// terminal.render("Hello, TUI world!").await?;
361///
362/// // Clean up (or rely on Drop)
363/// terminal.show_cursor().await?;
364/// terminal.exit_alt_screen().await?;
365/// terminal.exit_raw_mode().await?;
366/// # Ok(())
367/// # }
368/// ```
369pub struct Terminal {
370    raw_mode: bool,
371    alt_screen: bool,
372    mouse_enabled: bool,
373    focus_reporting: bool,
374    cursor_visible: bool,
375    output_writer: Option<Arc<Mutex<dyn AsyncWrite + Send + Unpin>>>,
376    /// Reusable buffer for string operations to minimize allocations
377    render_buffer: String,
378}
379
380impl Terminal {
381    /// Create a new [`Terminal`] instance.
382    ///
383    /// If an `output_writer` is provided, rendering is performed by writing to
384    /// that asynchronous writer instead of directly to stdout.
385    pub fn new(
386        output_writer: Option<Arc<Mutex<dyn AsyncWrite + Send + Unpin>>>,
387    ) -> Result<Self, Error> {
388        Ok(Self {
389            raw_mode: false,
390            alt_screen: false,
391            mouse_enabled: false,
392            focus_reporting: false,
393            cursor_visible: true,
394            output_writer,
395            render_buffer: String::with_capacity(8192), // Pre-allocate 8KB buffer
396        })
397    }
398}
399
400#[async_trait::async_trait]
401impl TerminalInterface for Terminal {
402    fn new(output_writer: Option<Arc<Mutex<dyn AsyncWrite + Send + Unpin>>>) -> Result<Self, Error>
403    where
404        Self: Sized,
405    {
406        Ok(Self {
407            raw_mode: false,
408            alt_screen: false,
409            mouse_enabled: false,
410            focus_reporting: false,
411            cursor_visible: true,
412            output_writer,
413            render_buffer: String::with_capacity(8192),
414        })
415    }
416
417    async fn enter_raw_mode(&mut self) -> Result<(), Error> {
418        if !self.raw_mode {
419            terminal::enable_raw_mode()?;
420            self.raw_mode = true;
421        }
422        Ok(())
423    }
424
425    async fn exit_raw_mode(&mut self) -> Result<(), Error> {
426        if self.raw_mode {
427            terminal::disable_raw_mode()?;
428            self.raw_mode = false;
429        }
430        Ok(())
431    }
432
433    async fn enter_alt_screen(&mut self) -> Result<(), Error> {
434        if !self.alt_screen {
435            execute!(io::stdout(), EnterAlternateScreen)?;
436            // Clear the alternate screen buffer immediately after entering
437            execute!(io::stdout(), terminal::Clear(terminal::ClearType::All))?;
438            io::stdout().flush()?;
439            self.alt_screen = true;
440        }
441        Ok(())
442    }
443
444    async fn exit_alt_screen(&mut self) -> Result<(), Error> {
445        if self.alt_screen {
446            execute!(io::stdout(), LeaveAlternateScreen)?;
447            io::stdout().flush()?;
448            self.alt_screen = false;
449        }
450        Ok(())
451    }
452
453    async fn enable_mouse(&mut self) -> Result<(), Error> {
454        if !self.mouse_enabled {
455            execute!(io::stdout(), EnableMouseCapture)?;
456            self.mouse_enabled = true;
457        }
458        Ok(())
459    }
460
461    async fn enable_mouse_cell_motion(&mut self) -> Result<(), Error> {
462        self.enable_mouse().await
463    }
464
465    async fn enable_mouse_all_motion(&mut self) -> Result<(), Error> {
466        self.enable_mouse().await
467    }
468
469    async fn disable_mouse(&mut self) -> Result<(), Error> {
470        if self.mouse_enabled {
471            execute!(io::stdout(), DisableMouseCapture)?;
472            self.mouse_enabled = false;
473        }
474        Ok(())
475    }
476
477    async fn enable_focus_reporting(&mut self) -> Result<(), Error> {
478        if !self.focus_reporting {
479            execute!(io::stdout(), EnableFocusChange)?;
480            self.focus_reporting = true;
481        }
482        Ok(())
483    }
484
485    async fn disable_focus_reporting(&mut self) -> Result<(), Error> {
486        if self.focus_reporting {
487            execute!(io::stdout(), DisableFocusChange)?;
488            self.focus_reporting = false;
489        }
490        Ok(())
491    }
492
493    async fn enable_bracketed_paste(&mut self) -> Result<(), Error> {
494        execute!(io::stdout(), EnableBracketedPaste)?;
495        Ok(())
496    }
497
498    async fn disable_bracketed_paste(&mut self) -> Result<(), Error> {
499        execute!(io::stdout(), DisableBracketedPaste)?;
500        Ok(())
501    }
502
503    async fn show_cursor(&mut self) -> Result<(), Error> {
504        if !self.cursor_visible {
505            execute!(io::stdout(), Show)?;
506            self.cursor_visible = true;
507        }
508        Ok(())
509    }
510
511    async fn hide_cursor(&mut self) -> Result<(), Error> {
512        if self.cursor_visible {
513            execute!(io::stdout(), Hide)?;
514            self.cursor_visible = false;
515        }
516        Ok(())
517    }
518
519    async fn clear(&mut self) -> Result<(), Error> {
520        execute!(io::stdout(), terminal::Clear(terminal::ClearType::All))?;
521        Ok(())
522    }
523
524    async fn render(&mut self, content: &str) -> Result<(), Error> {
525        use crossterm::cursor::MoveTo;
526        use crossterm::terminal::{Clear, ClearType};
527
528        if let Some(writer) = &mut self.output_writer {
529            use tokio::io::AsyncWriteExt;
530
531            // Pre-allocate buffer for efficient rendering
532            self.render_buffer.clear();
533
534            // Reserve space for the clear sequence plus content
535            let estimated_size = 8 + content.len() + content.chars().filter(|&c| c == '\n').count();
536            self.render_buffer.reserve(estimated_size);
537
538            // Add clear sequence
539            self.render_buffer.push_str("\x1b[H\x1b[2J");
540
541            // Efficiently replace newlines by iterating through chars
542            for ch in content.chars() {
543                if ch == '\n' {
544                    self.render_buffer.push_str("\r\n");
545                } else {
546                    self.render_buffer.push(ch);
547                }
548            }
549
550            writer
551                .lock()
552                .await
553                .write_all(self.render_buffer.as_bytes())
554                .await?;
555            writer.lock().await.flush().await?;
556        } else {
557            // Move cursor to top-left and clear entire screen
558            execute!(io::stdout(), MoveTo(0, 0))?;
559            execute!(io::stdout(), Clear(ClearType::All))?;
560
561            // Pre-allocate buffer for efficient rendering
562            self.render_buffer.clear();
563
564            // Reserve space for content plus newline replacements
565            let estimated_size = content.len() + content.chars().filter(|&c| c == '\n').count();
566            self.render_buffer.reserve(estimated_size);
567
568            // Efficiently replace newlines by iterating through chars
569            for ch in content.chars() {
570                if ch == '\n' {
571                    self.render_buffer.push_str("\r\n");
572                } else {
573                    self.render_buffer.push(ch);
574                }
575            }
576
577            print!("{}", self.render_buffer);
578            io::stdout().flush()?;
579        }
580        Ok(())
581    }
582
583    fn size(&self) -> Result<(u16, u16), Error> {
584        let (width, height) = terminal::size()?;
585        Ok((width, height))
586    }
587}
588
589impl Drop for Terminal {
590    fn drop(&mut self) {
591        if !self.cursor_visible {
592            let _ = execute!(io::stdout(), Show);
593        }
594        if self.mouse_enabled {
595            let _ = execute!(io::stdout(), DisableMouseCapture);
596        }
597        if self.focus_reporting {
598            let _ = execute!(io::stdout(), DisableFocusChange);
599        }
600        if self.alt_screen {
601            let _ = execute!(io::stdout(), LeaveAlternateScreen);
602            let _ = io::stdout().flush();
603        }
604        if self.raw_mode {
605            let _ = terminal::disable_raw_mode();
606        }
607    }
608}
609
610/// A no-op terminal implementation useful for tests and headless operation.
611///
612/// This terminal implementation provides the `TerminalInterface` without
613/// actually performing any terminal operations. It's designed for testing,
614/// headless environments, or situations where terminal control is not needed.
615///
616/// # Use Cases
617///
618/// - Unit testing TUI applications without requiring a real terminal
619/// - Running applications in headless environments
620/// - Debugging and development scenarios
621/// - Performance testing without terminal I/O overhead
622///
623/// # Behavior
624///
625/// - All terminal control methods return success without doing anything
626/// - `render()` writes to the output writer if provided, otherwise does nothing
627/// - `size()` returns `(0, 0)` as a placeholder
628///
629/// # Example
630///
631/// ```rust
632/// use bubbletea_rs::terminal::{DummyTerminal, TerminalInterface};
633/// use bubbletea_rs::Error;
634/// use std::sync::Arc;
635/// use tokio::sync::Mutex;
636///
637/// # async fn example() -> Result<(), Error> {
638/// // Create with no output (all operations are no-ops)
639/// let mut dummy = DummyTerminal::new(None)?;
640///
641/// // These all succeed but do nothing
642/// dummy.enter_raw_mode().await?;
643/// dummy.hide_cursor().await?;
644/// dummy.render("This won't be displayed").await?;
645/// # Ok(())
646/// # }
647/// ```
648pub struct DummyTerminal {
649    output_writer: Option<Arc<Mutex<dyn AsyncWrite + Send + Unpin>>>,
650}
651
652#[async_trait::async_trait]
653impl TerminalInterface for DummyTerminal {
654    fn new(
655        output_writer: Option<Arc<Mutex<dyn AsyncWrite + Send + Unpin>>>,
656    ) -> Result<Self, Error> {
657        Ok(Self { output_writer })
658    }
659    async fn enter_raw_mode(&mut self) -> Result<(), Error> {
660        Ok(())
661    }
662    async fn exit_raw_mode(&mut self) -> Result<(), Error> {
663        Ok(())
664    }
665    async fn enter_alt_screen(&mut self) -> Result<(), Error> {
666        Ok(())
667    }
668    async fn exit_alt_screen(&mut self) -> Result<(), Error> {
669        Ok(())
670    }
671    async fn enable_mouse(&mut self) -> Result<(), Error> {
672        Ok(())
673    }
674    async fn enable_mouse_cell_motion(&mut self) -> Result<(), Error> {
675        Ok(())
676    }
677    async fn enable_mouse_all_motion(&mut self) -> Result<(), Error> {
678        Ok(())
679    }
680    async fn disable_mouse(&mut self) -> Result<(), Error> {
681        Ok(())
682    }
683    async fn enable_focus_reporting(&mut self) -> Result<(), Error> {
684        Ok(())
685    }
686    async fn disable_focus_reporting(&mut self) -> Result<(), Error> {
687        Ok(())
688    }
689    async fn enable_bracketed_paste(&mut self) -> Result<(), Error> {
690        Ok(())
691    }
692    async fn disable_bracketed_paste(&mut self) -> Result<(), Error> {
693        Ok(())
694    }
695    async fn show_cursor(&mut self) -> Result<(), Error> {
696        Ok(())
697    }
698    async fn hide_cursor(&mut self) -> Result<(), Error> {
699        Ok(())
700    }
701    async fn clear(&mut self) -> Result<(), Error> {
702        Ok(())
703    }
704    async fn render(&mut self, content: &str) -> Result<(), Error> {
705        if let Some(writer) = &mut self.output_writer {
706            use tokio::io::AsyncWriteExt;
707            writer.lock().await.write_all(content.as_bytes()).await?;
708            writer.lock().await.flush().await?;
709        }
710        Ok(())
711    }
712    fn size(&self) -> Result<(u16, u16), Error> {
713        Ok((0, 0))
714    }
715}