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}