Skip to main content

aura_core/effects/
terminal.rs

1//! Terminal effect interface for deterministic TUI/CLI testing
2//!
3//! # Effect Classification
4//!
5//! - **Category**: Infrastructure Effect
6//! - **Implementation**: `aura-effects` (Layer 3) for production, `aura-testkit` (Layer 8) for testing
7//! - **Usage**: `aura-terminal` for TUI and CLI testing
8//!
9//! This module abstracts terminal I/O to enable:
10//! - Deterministic event injection for testing
11//! - Frame capture for output verification
12//! - State machine modeling for formal verification (Quint)
13//! - Reproducible test execution
14//!
15//! # Architecture
16//!
17//! The TUI is modeled as a pure state machine:
18//! ```text
19//! TuiState × TerminalEvent → (TuiState, RenderOutput, Commands)
20//! ```
21//!
22//! By abstracting terminal I/O into effect traits, tests can:
23//! 1. Inject predetermined event sequences
24//! 2. Capture all rendered frames
25//! 3. Assert on exact output
26//! 4. Replay failing scenarios deterministically
27
28use crate::AuraError;
29use async_trait::async_trait;
30use serde::{Deserialize, Serialize};
31use std::fmt;
32
33// ============================================================================
34// Terminal Events
35// ============================================================================
36
37/// Terminal event - the input to the TUI state machine
38#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
39pub enum TerminalEvent {
40    /// Keyboard event
41    Key(KeyEvent),
42    /// Mouse event
43    Mouse(MouseEvent),
44    /// Terminal resize event
45    Resize { width: u16, height: u16 },
46    /// Focus gained
47    FocusGained,
48    /// Focus lost
49    FocusLost,
50    /// Paste event (bracketed paste)
51    Paste(String),
52    /// Tick event for time-based updates (e.g., animations, timeouts)
53    Tick,
54}
55
56/// Keyboard event
57#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
58pub struct KeyEvent {
59    /// The key code
60    pub code: KeyCode,
61    /// Key modifiers (Ctrl, Alt, Shift)
62    pub modifiers: Modifiers,
63    /// Event kind (Press, Release, Repeat)
64    pub kind: KeyEventKind,
65}
66
67impl KeyEvent {
68    /// Create a simple key press event
69    pub fn press(code: KeyCode) -> Self {
70        Self {
71            code,
72            modifiers: Modifiers::NONE,
73            kind: KeyEventKind::Press,
74        }
75    }
76
77    /// Create a key press with modifiers
78    pub fn press_with(code: KeyCode, modifiers: Modifiers) -> Self {
79        Self {
80            code,
81            modifiers,
82            kind: KeyEventKind::Press,
83        }
84    }
85
86    /// Create a character key press
87    pub fn char(c: char) -> Self {
88        Self::press(KeyCode::Char(c))
89    }
90
91    /// Create a character key press with Ctrl modifier
92    pub fn ctrl(c: char) -> Self {
93        Self::press_with(KeyCode::Char(c), Modifiers::CTRL)
94    }
95}
96
97/// Key code - matches crossterm/iocraft key codes
98#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
99pub enum KeyCode {
100    /// Backspace key
101    Backspace,
102    /// Enter/Return key
103    Enter,
104    /// Left arrow
105    Left,
106    /// Right arrow
107    Right,
108    /// Up arrow
109    Up,
110    /// Down arrow
111    Down,
112    /// Home key
113    Home,
114    /// End key
115    End,
116    /// Page Up
117    PageUp,
118    /// Page Down
119    PageDown,
120    /// Tab key
121    Tab,
122    /// Backtab (Shift+Tab)
123    BackTab,
124    /// Delete key
125    Delete,
126    /// Insert key
127    Insert,
128    /// Function key (F1-F12)
129    F(u8),
130    /// Character key
131    Char(char),
132    /// Null character (Ctrl+Space on some terminals)
133    Null,
134    /// Escape key
135    Esc,
136    /// Caps Lock
137    CapsLock,
138    /// Scroll Lock
139    ScrollLock,
140    /// Num Lock
141    NumLock,
142    /// Print Screen
143    PrintScreen,
144    /// Pause
145    Pause,
146    /// Menu key
147    Menu,
148    /// Keypad Begin
149    KeypadBegin,
150}
151
152/// Key event kind
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
154pub enum KeyEventKind {
155    /// Key press
156    #[default]
157    Press,
158    /// Key release
159    Release,
160    /// Key repeat (held down)
161    Repeat,
162}
163
164/// Key modifiers (bitflags-style)
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
166pub struct Modifiers(u8);
167
168impl Modifiers {
169    /// No modifiers
170    pub const NONE: Self = Self(0);
171    /// Shift key
172    pub const SHIFT: Self = Self(1 << 0);
173    /// Ctrl key
174    pub const CTRL: Self = Self(1 << 1);
175    /// Alt key
176    pub const ALT: Self = Self(1 << 2);
177    /// Super/Meta/Windows key
178    pub const SUPER: Self = Self(1 << 3);
179    /// Hyper key
180    pub const HYPER: Self = Self(1 << 4);
181    /// Meta key (distinct from Super on some systems)
182    pub const META: Self = Self(1 << 5);
183
184    /// Check if shift is pressed
185    pub fn shift(self) -> bool {
186        self.0 & Self::SHIFT.0 != 0
187    }
188
189    /// Check if ctrl is pressed
190    pub fn ctrl(self) -> bool {
191        self.0 & Self::CTRL.0 != 0
192    }
193
194    /// Check if alt is pressed
195    pub fn alt(self) -> bool {
196        self.0 & Self::ALT.0 != 0
197    }
198
199    /// Check if super/command key is pressed
200    pub fn super_key(self) -> bool {
201        self.0 & Self::SUPER.0 != 0
202    }
203
204    /// Combine modifiers
205    pub fn union(self, other: Self) -> Self {
206        Self(self.0 | other.0)
207    }
208}
209
210impl std::ops::BitOr for Modifiers {
211    type Output = Self;
212    fn bitor(self, rhs: Self) -> Self {
213        Self(self.0 | rhs.0)
214    }
215}
216
217/// Mouse event
218#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
219pub struct MouseEvent {
220    /// Mouse button or action
221    pub kind: MouseEventKind,
222    /// Column (x coordinate)
223    pub column: u16,
224    /// Row (y coordinate)
225    pub row: u16,
226    /// Key modifiers held during mouse event
227    pub modifiers: Modifiers,
228}
229
230/// Mouse event kind
231#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
232pub enum MouseEventKind {
233    /// Mouse button pressed
234    Down(MouseButton),
235    /// Mouse button released
236    Up(MouseButton),
237    /// Mouse moved while button held (drag)
238    Drag(MouseButton),
239    /// Mouse moved (no button held)
240    Moved,
241    /// Scroll up
242    ScrollUp,
243    /// Scroll down
244    ScrollDown,
245    /// Scroll left
246    ScrollLeft,
247    /// Scroll right
248    ScrollRight,
249}
250
251/// Mouse button
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
253pub enum MouseButton {
254    /// Left mouse button
255    Left,
256    /// Right mouse button
257    Right,
258    /// Middle mouse button
259    Middle,
260}
261
262// ============================================================================
263// Terminal Frame (Output Capture)
264// ============================================================================
265
266/// A captured terminal frame - the output of a render cycle
267#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
268pub struct TerminalFrame {
269    /// Frame width in columns
270    pub width: u16,
271    /// Frame height in rows
272    pub height: u16,
273    /// Cell buffer (row-major: cells[row][col])
274    pub cells: Vec<Vec<Cell>>,
275    /// Cursor position (if visible)
276    pub cursor: Option<CursorPosition>,
277    /// Frame sequence number (for ordering)
278    pub sequence: u64,
279}
280
281impl TerminalFrame {
282    /// Create a new empty frame
283    pub fn new(width: u16, height: u16) -> Self {
284        let cells = (0..height)
285            .map(|_| (0..width).map(|_| Cell::default()).collect())
286            .collect();
287        Self {
288            width,
289            height,
290            cells,
291            cursor: None,
292            sequence: 0,
293        }
294    }
295
296    /// Get a cell at (column, row)
297    pub fn get(&self, col: u16, row: u16) -> Option<&Cell> {
298        self.cells.get(row as usize)?.get(col as usize)
299    }
300
301    /// Set a cell at (column, row)
302    pub fn set(&mut self, col: u16, row: u16, cell: Cell) {
303        if let Some(row_cells) = self.cells.get_mut(row as usize) {
304            if let Some(existing) = row_cells.get_mut(col as usize) {
305                *existing = cell;
306            }
307        }
308    }
309
310    /// Extract text content from a row
311    pub fn row_text(&self, row: u16) -> String {
312        self.cells
313            .get(row as usize)
314            .map(|cells| cells.iter().map(|c| c.char).collect())
315            .unwrap_or_default()
316    }
317
318    /// Extract all text content (rows joined by newlines)
319    pub fn text(&self) -> String {
320        self.cells
321            .iter()
322            .map(|row| row.iter().map(|c| c.char).collect::<String>())
323            .collect::<Vec<_>>()
324            .join("\n")
325    }
326
327    /// Check if frame contains text anywhere
328    pub fn contains(&self, text: &str) -> bool {
329        // Check each row
330        for row in &self.cells {
331            let row_text: String = row.iter().map(|c| c.char).collect();
332            if row_text.contains(text) {
333                return true;
334            }
335        }
336        // Also check full text (for multi-line)
337        self.text().contains(text)
338    }
339
340    /// Find position of text in frame
341    pub fn find(&self, text: &str) -> Option<(u16, u16)> {
342        for (row_idx, row) in self.cells.iter().enumerate() {
343            let row_text: String = row.iter().map(|c| c.char).collect();
344            if let Some(col_idx) = row_text.find(text) {
345                return Some((col_idx as u16, row_idx as u16));
346            }
347        }
348        None
349    }
350}
351
352impl fmt::Display for TerminalFrame {
353    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
354        for row in &self.cells {
355            for cell in row {
356                write!(f, "{}", cell.char)?;
357            }
358            writeln!(f)?;
359        }
360        Ok(())
361    }
362}
363
364/// A single cell in the terminal
365#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
366pub struct Cell {
367    /// The character displayed
368    pub char: char,
369    /// Foreground color
370    pub fg: Color,
371    /// Background color
372    pub bg: Color,
373    /// Text style modifiers
374    pub style: Style,
375}
376
377impl Default for Cell {
378    fn default() -> Self {
379        Self {
380            char: ' ',
381            fg: Color::Reset,
382            bg: Color::Reset,
383            style: Style::empty(),
384        }
385    }
386}
387
388impl Cell {
389    /// Create a cell with a character
390    pub fn new(c: char) -> Self {
391        Self {
392            char: c,
393            ..Default::default()
394        }
395    }
396
397    /// Create a cell with character and foreground color
398    #[must_use]
399    pub fn with_fg(c: char, fg: Color) -> Self {
400        Self {
401            char: c,
402            fg,
403            ..Default::default()
404        }
405    }
406}
407
408/// Terminal colors
409#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
410pub enum Color {
411    /// Reset to default
412    #[default]
413    Reset,
414    /// Black
415    Black,
416    /// Dark red
417    DarkRed,
418    /// Dark green
419    DarkGreen,
420    /// Dark yellow
421    DarkYellow,
422    /// Dark blue
423    DarkBlue,
424    /// Dark magenta
425    DarkMagenta,
426    /// Dark cyan
427    DarkCyan,
428    /// Gray
429    Gray,
430    /// Dark gray
431    DarkGray,
432    /// Red
433    Red,
434    /// Green
435    Green,
436    /// Yellow
437    Yellow,
438    /// Blue
439    Blue,
440    /// Magenta
441    Magenta,
442    /// Cyan
443    Cyan,
444    /// White
445    White,
446    /// 256-color palette
447    Indexed(u8),
448    /// True color RGB
449    Rgb { r: u8, g: u8, b: u8 },
450}
451
452/// Text style modifiers (bitflags-style)
453#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
454pub struct Style(u16);
455
456impl Style {
457    /// No style
458    pub const fn empty() -> Self {
459        Self(0)
460    }
461
462    /// Bold text
463    pub const BOLD: Self = Self(1 << 0);
464    /// Dim text
465    pub const DIM: Self = Self(1 << 1);
466    /// Italic text
467    pub const ITALIC: Self = Self(1 << 2);
468    /// Underlined text
469    pub const UNDERLINED: Self = Self(1 << 3);
470    /// Slow blink
471    pub const SLOW_BLINK: Self = Self(1 << 4);
472    /// Rapid blink
473    pub const RAPID_BLINK: Self = Self(1 << 5);
474    /// Reversed colors
475    pub const REVERSED: Self = Self(1 << 6);
476    /// Hidden text
477    pub const HIDDEN: Self = Self(1 << 7);
478    /// Crossed out text
479    pub const CROSSED_OUT: Self = Self(1 << 8);
480
481    /// Check if bold
482    pub fn is_bold(self) -> bool {
483        self.0 & Self::BOLD.0 != 0
484    }
485
486    /// Check if underlined
487    pub fn is_underlined(self) -> bool {
488        self.0 & Self::UNDERLINED.0 != 0
489    }
490
491    /// Combine styles
492    pub fn union(self, other: Self) -> Self {
493        Self(self.0 | other.0)
494    }
495}
496
497impl std::ops::BitOr for Style {
498    type Output = Self;
499    fn bitor(self, rhs: Self) -> Self {
500        Self(self.0 | rhs.0)
501    }
502}
503
504/// Cursor position and state
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
506pub struct CursorPosition {
507    /// Column (x)
508    pub col: u16,
509    /// Row (y)
510    pub row: u16,
511    /// Cursor shape
512    pub shape: CursorShape,
513    /// Whether cursor is blinking
514    pub blinking: bool,
515}
516
517/// Cursor shape
518#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
519pub enum CursorShape {
520    /// Block cursor (full cell)
521    #[default]
522    Block,
523    /// Underline cursor
524    Underline,
525    /// Bar/line cursor (vertical line)
526    Bar,
527}
528
529// ============================================================================
530// Terminal Error
531// ============================================================================
532
533/// Terminal operation error
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
535pub enum TerminalError {
536    /// No more events available (for testing)
537    EndOfInput,
538    /// Terminal I/O error
539    IoError(String),
540    /// Terminal not available (headless mode without mock)
541    NotAvailable,
542    /// Invalid operation for current state
543    InvalidOperation(String),
544    /// Timeout waiting for event
545    Timeout,
546}
547
548impl fmt::Display for TerminalError {
549    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
550        match self {
551            Self::EndOfInput => write!(f, "end of input"),
552            Self::IoError(msg) => write!(f, "terminal I/O error: {msg}"),
553            Self::NotAvailable => write!(f, "terminal not available"),
554            Self::InvalidOperation(msg) => write!(f, "invalid operation: {msg}"),
555            Self::Timeout => write!(f, "timeout waiting for terminal event"),
556        }
557    }
558}
559
560impl std::error::Error for TerminalError {}
561
562impl From<TerminalError> for AuraError {
563    fn from(err: TerminalError) -> Self {
564        AuraError::Terminal(err.to_string())
565    }
566}
567
568// ============================================================================
569// Terminal Effect Traits
570// ============================================================================
571
572/// Terminal input effects - abstracts reading events from terminal
573///
574/// # Implementations
575///
576/// - **Production**: Wraps iocraft/crossterm terminal event stream
577/// - **Testing**: `MockTerminalHandler` with predetermined event queue
578/// - **Simulation**: Seeded random event generation
579#[async_trait]
580pub trait TerminalInputEffects: Send + Sync {
581    /// Get the next terminal event, blocking until available
582    ///
583    /// Returns `Err(TerminalError::EndOfInput)` when no more events
584    /// are available (useful for testing with finite event sequences).
585    async fn next_event(&self) -> Result<TerminalEvent, TerminalError>;
586
587    /// Poll for an event with timeout
588    ///
589    /// Returns `Ok(None)` if no event within timeout.
590    /// Returns `Ok(Some(event))` if event received.
591    /// Returns `Err(...)` on error.
592    async fn poll_event(&self, timeout_ms: u64) -> Result<Option<TerminalEvent>, TerminalError>;
593
594    /// Check if input is available without blocking
595    async fn has_input(&self) -> bool;
596}
597
598/// Terminal output effects - abstracts rendering to terminal
599///
600/// # Implementations
601///
602/// - **Production**: Wraps iocraft terminal rendering
603/// - **Testing**: `MockTerminalHandler` captures frames for assertion
604/// - **Simulation**: Tracks render calls without actual output
605#[async_trait]
606pub trait TerminalOutputEffects: Send + Sync {
607    /// Render a frame to the terminal
608    ///
609    /// In production, this writes to the real terminal.
610    /// In testing, this captures the frame for later assertion.
611    async fn render(&self, frame: TerminalFrame) -> Result<(), TerminalError>;
612
613    /// Get terminal dimensions
614    async fn size(&self) -> Result<(u16, u16), TerminalError>;
615
616    /// Clear the terminal
617    async fn clear(&self) -> Result<(), TerminalError>;
618
619    /// Set cursor position
620    async fn set_cursor(&self, col: u16, row: u16) -> Result<(), TerminalError>;
621
622    /// Show or hide cursor
623    async fn set_cursor_visible(&self, visible: bool) -> Result<(), TerminalError>;
624
625    /// Set cursor shape
626    async fn set_cursor_shape(&self, shape: CursorShape) -> Result<(), TerminalError>;
627
628    /// Enter alternate screen buffer
629    async fn enter_alternate_screen(&self) -> Result<(), TerminalError>;
630
631    /// Leave alternate screen buffer
632    async fn leave_alternate_screen(&self) -> Result<(), TerminalError>;
633
634    /// Enable raw mode (no line buffering, no echo)
635    async fn enable_raw_mode(&self) -> Result<(), TerminalError>;
636
637    /// Disable raw mode
638    async fn disable_raw_mode(&self) -> Result<(), TerminalError>;
639}
640
641/// Combined terminal effects
642pub trait TerminalEffects: TerminalInputEffects + TerminalOutputEffects {}
643
644/// Blanket implementation for types that implement both traits
645impl<T: TerminalInputEffects + TerminalOutputEffects> TerminalEffects for T {}
646
647// ============================================================================
648// Blanket Implementations for Arc<T>
649// ============================================================================
650
651#[async_trait]
652impl<T: TerminalInputEffects + ?Sized> TerminalInputEffects for std::sync::Arc<T> {
653    async fn next_event(&self) -> Result<TerminalEvent, TerminalError> {
654        (**self).next_event().await
655    }
656
657    async fn poll_event(&self, timeout_ms: u64) -> Result<Option<TerminalEvent>, TerminalError> {
658        (**self).poll_event(timeout_ms).await
659    }
660
661    async fn has_input(&self) -> bool {
662        (**self).has_input().await
663    }
664}
665
666#[async_trait]
667impl<T: TerminalOutputEffects + ?Sized> TerminalOutputEffects for std::sync::Arc<T> {
668    async fn render(&self, frame: TerminalFrame) -> Result<(), TerminalError> {
669        (**self).render(frame).await
670    }
671
672    async fn size(&self) -> Result<(u16, u16), TerminalError> {
673        (**self).size().await
674    }
675
676    async fn clear(&self) -> Result<(), TerminalError> {
677        (**self).clear().await
678    }
679
680    async fn set_cursor(&self, col: u16, row: u16) -> Result<(), TerminalError> {
681        (**self).set_cursor(col, row).await
682    }
683
684    async fn set_cursor_visible(&self, visible: bool) -> Result<(), TerminalError> {
685        (**self).set_cursor_visible(visible).await
686    }
687
688    async fn set_cursor_shape(&self, shape: CursorShape) -> Result<(), TerminalError> {
689        (**self).set_cursor_shape(shape).await
690    }
691
692    async fn enter_alternate_screen(&self) -> Result<(), TerminalError> {
693        (**self).enter_alternate_screen().await
694    }
695
696    async fn leave_alternate_screen(&self) -> Result<(), TerminalError> {
697        (**self).leave_alternate_screen().await
698    }
699
700    async fn enable_raw_mode(&self) -> Result<(), TerminalError> {
701        (**self).enable_raw_mode().await
702    }
703
704    async fn disable_raw_mode(&self) -> Result<(), TerminalError> {
705        (**self).disable_raw_mode().await
706    }
707}
708
709// ============================================================================
710// Event Builder Helpers (for testing convenience)
711// ============================================================================
712
713/// Helper functions for constructing terminal events in tests
714pub mod events {
715    use super::*;
716
717    /// Create a key press event for a character
718    pub fn char(c: char) -> TerminalEvent {
719        TerminalEvent::Key(KeyEvent::char(c))
720    }
721
722    /// Create a key press event
723    pub fn key(code: KeyCode) -> TerminalEvent {
724        TerminalEvent::Key(KeyEvent::press(code))
725    }
726
727    /// Create an Enter key press
728    pub fn enter() -> TerminalEvent {
729        key(KeyCode::Enter)
730    }
731
732    /// Create an Escape key press
733    pub fn escape() -> TerminalEvent {
734        key(KeyCode::Esc)
735    }
736
737    /// Create a Tab key press
738    pub fn tab() -> TerminalEvent {
739        key(KeyCode::Tab)
740    }
741
742    /// Create a Backspace key press
743    pub fn backspace() -> TerminalEvent {
744        key(KeyCode::Backspace)
745    }
746
747    /// Create an arrow key press
748    pub fn arrow_up() -> TerminalEvent {
749        key(KeyCode::Up)
750    }
751
752    /// Create an arrow key press
753    pub fn arrow_down() -> TerminalEvent {
754        key(KeyCode::Down)
755    }
756
757    /// Create an arrow key press
758    pub fn arrow_left() -> TerminalEvent {
759        key(KeyCode::Left)
760    }
761
762    /// Create an arrow key press
763    pub fn arrow_right() -> TerminalEvent {
764        key(KeyCode::Right)
765    }
766
767    /// Create a Ctrl+key press
768    pub fn ctrl(c: char) -> TerminalEvent {
769        TerminalEvent::Key(KeyEvent::ctrl(c))
770    }
771
772    /// Create a function key press (F1-F12)
773    pub fn function(n: u8) -> TerminalEvent {
774        key(KeyCode::F(n))
775    }
776
777    /// Create a resize event
778    pub fn resize(width: u16, height: u16) -> TerminalEvent {
779        TerminalEvent::Resize { width, height }
780    }
781
782    /// Create a tick event
783    pub fn tick() -> TerminalEvent {
784        TerminalEvent::Tick
785    }
786
787    /// Type a string as a sequence of character events
788    pub fn type_str(s: &str) -> Vec<TerminalEvent> {
789        s.chars().map(char).collect()
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796
797    #[test]
798    fn test_key_event_creation() {
799        let event = KeyEvent::char('a');
800        assert_eq!(event.code, KeyCode::Char('a'));
801        assert_eq!(event.modifiers, Modifiers::NONE);
802        assert_eq!(event.kind, KeyEventKind::Press);
803    }
804
805    #[test]
806    fn test_ctrl_key() {
807        let event = KeyEvent::ctrl('c');
808        assert_eq!(event.code, KeyCode::Char('c'));
809        assert!(event.modifiers.ctrl());
810    }
811
812    #[test]
813    fn test_frame_contains() {
814        let mut frame = TerminalFrame::new(20, 5);
815        // Write "Hello" to row 1
816        for (i, c) in "Hello".chars().enumerate() {
817            frame.set(i as u16, 1, Cell::new(c));
818        }
819
820        assert!(frame.contains("Hello"));
821        assert!(frame.contains("ell"));
822        assert!(!frame.contains("World"));
823    }
824
825    #[test]
826    fn test_frame_find() {
827        let mut frame = TerminalFrame::new(20, 5);
828        // Write "Test" to row 2, column 5
829        for (i, c) in "Test".chars().enumerate() {
830            frame.set(5 + i as u16, 2, Cell::new(c));
831        }
832
833        assert_eq!(frame.find("Test"), Some((5, 2)));
834        assert_eq!(frame.find("Missing"), None);
835    }
836
837    #[test]
838    fn test_event_helpers() {
839        use events::*;
840
841        let events = [char('h'), char('i'), enter(), escape()];
842
843        assert_eq!(events.len(), 4);
844
845        let typed = type_str("hello");
846        assert_eq!(typed.len(), 5);
847    }
848
849    #[test]
850    fn test_modifiers() {
851        let ctrl_shift = Modifiers::CTRL | Modifiers::SHIFT;
852        assert!(ctrl_shift.ctrl());
853        assert!(ctrl_shift.shift());
854        assert!(!ctrl_shift.alt());
855    }
856}