Skip to main content

presentar_terminal/
app.rs

1//! TUI application runner with Jidoka verification gates.
2//!
3//! ## Non-Blocking UI Pattern (CB-INPUT-006)
4//!
5//! For applications with heavy data collection (system monitors, dashboards),
6//! use the [`AsyncCollector`] pattern to ensure the main thread never blocks.
7//!
8//! ```ignore
9//! // Background thread owns collectors, sends snapshots through channel
10//! let (tx, rx) = mpsc::channel::<MySnapshot>();
11//!
12//! std::thread::spawn(move || {
13//!     let mut collector = MyCollector::new();
14//!     loop {
15//!         let snapshot = collector.collect();  // Can take seconds
16//!         tx.send(snapshot).ok();
17//!         std::thread::sleep(Duration::from_secs(1));
18//!     }
19//! });
20//!
21//! // Main thread: input + render only (always <16ms)
22//! loop {
23//!     while let Ok(snapshot) = rx.try_recv() {
24//!         app.apply_snapshot(snapshot);  // O(1) operation
25//!     }
26//!     app.handle_input();  // Non-blocking
27//!     app.render();        // <16ms budget
28//! }
29//! ```
30
31#![allow(dead_code, unreachable_pub)]
32
33use crate::color::ColorMode;
34use crate::direct::{CellBuffer, DiffRenderer, DirectTerminalCanvas};
35use crate::error::{TuiError, VerificationError};
36use crate::input::InputHandler;
37use crossterm::{
38    cursor,
39    event::{self, Event as CrosstermEvent, KeyCode},
40    execute,
41    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
42};
43use presentar_core::{Constraints, Rect, Widget};
44use std::io::{self, Stdout, Write};
45use std::time::{Duration, Instant};
46
47// =============================================================================
48// Non-Blocking UI Pattern (CB-INPUT-006)
49// =============================================================================
50
51/// Snapshot of collected metrics, transportable via channel.
52///
53/// Implement this trait for data structures that are sent from a background
54/// collector thread to the main UI thread.
55///
56/// # Requirements
57/// - Must be `Clone` (for potential buffering)
58/// - Must be `Send` (for channel transport)
59/// - Must be `'static` (for thread safety)
60pub trait Snapshot: Clone + Send + 'static {
61    /// Create an empty snapshot for initial state before first collection.
62    fn empty() -> Self;
63}
64
65/// Background collector that produces snapshots.
66///
67/// Implement this trait for objects that collect metrics in a background thread.
68/// The collector owns all heavy I/O objects (System, Disks, Networks, etc.)
69/// and produces lightweight snapshots that can be sent through a channel.
70///
71/// # Example
72/// ```ignore
73/// struct SystemCollector {
74///     system: System,
75///     disks: Disks,
76/// }
77///
78/// impl AsyncCollector for SystemCollector {
79///     type Snapshot = MetricsSnapshot;
80///
81///     fn collect(&mut self) -> MetricsSnapshot {
82///         self.system.refresh_all();  // Heavy I/O
83///         MetricsSnapshot {
84///             cpu_usage: self.system.global_cpu_usage(),
85///             // ... extract other data
86///         }
87///     }
88/// }
89/// ```
90pub trait AsyncCollector: Send + 'static {
91    /// The snapshot type produced by this collector.
92    type Snapshot: Snapshot;
93
94    /// Collect metrics and return a snapshot.
95    ///
96    /// This method may take seconds to complete (heavy I/O).
97    /// It runs in a background thread, never blocking the UI.
98    fn collect(&mut self) -> Self::Snapshot;
99}
100
101/// Application that can apply snapshots to update its state.
102///
103/// Implement this trait for your application state. The `apply_snapshot`
104/// method is called on the main thread and MUST complete in <1ms.
105///
106/// # Example
107/// ```ignore
108/// impl SnapshotReceiver for MyApp {
109///     type Snapshot = MetricsSnapshot;
110///
111///     fn apply_snapshot(&mut self, snapshot: MetricsSnapshot) {
112///         // O(1) operations only - just copy/swap data
113///         self.cpu_usage = snapshot.cpu_usage;
114///         self.processes = snapshot.processes;
115///     }
116/// }
117/// ```
118pub trait SnapshotReceiver {
119    /// The snapshot type this receiver accepts.
120    type Snapshot: Snapshot;
121
122    /// Apply a snapshot to update the application state.
123    ///
124    /// **MUST be O(1) and complete in <1ms.**
125    /// Only perform simple assignments, no I/O or heavy computation.
126    fn apply_snapshot(&mut self, snapshot: Self::Snapshot);
127}
128
129/// QA timing diagnostics for non-blocking UI verification.
130///
131/// Use this struct to collect timing data for `--qa-timing` output.
132#[derive(Debug, Clone, Default)]
133pub struct QaTimings {
134    /// Input event processing times in microseconds.
135    pub input_times_us: Vec<u64>,
136    /// Lock acquisition times in microseconds (should be 0 with channel pattern).
137    pub lock_times_us: Vec<u64>,
138    /// Render times in microseconds.
139    pub render_times_us: Vec<u64>,
140    /// Last collect duration in microseconds (from background thread).
141    pub last_collect_us: u64,
142}
143
144impl QaTimings {
145    /// Create new QA timing collector.
146    #[must_use]
147    pub fn new() -> Self {
148        Self::default()
149    }
150
151    /// Record an input event processing time.
152    pub fn record_input(&mut self, duration: Duration) {
153        self.input_times_us.push(duration.as_micros() as u64);
154    }
155
156    /// Record a lock acquisition time.
157    pub fn record_lock(&mut self, duration: Duration) {
158        self.lock_times_us.push(duration.as_micros() as u64);
159    }
160
161    /// Record a render time.
162    pub fn record_render(&mut self, duration: Duration) {
163        self.render_times_us.push(duration.as_micros() as u64);
164    }
165
166    /// Format timing report for stderr output.
167    #[must_use]
168    pub fn format_report(&self) -> String {
169        let avg = |v: &[u64]| {
170            if v.is_empty() {
171                0
172            } else {
173                v.iter().sum::<u64>() / v.len() as u64
174            }
175        };
176        let max = |v: &[u64]| v.iter().max().copied().unwrap_or(0);
177
178        format!(
179            "[QA] input: avg={}us max={}us | lock: avg={}us max={}us | render: avg={}us max={}us | collect: {}us",
180            avg(&self.input_times_us), max(&self.input_times_us),
181            avg(&self.lock_times_us), max(&self.lock_times_us),
182            avg(&self.render_times_us), max(&self.render_times_us),
183            self.last_collect_us
184        )
185    }
186
187    /// Clear accumulated timing data.
188    pub fn clear(&mut self) {
189        self.input_times_us.clear();
190        self.lock_times_us.clear();
191        self.render_times_us.clear();
192    }
193}
194
195// =============================================================================
196// Terminal Abstraction
197// =============================================================================
198
199/// Terminal abstraction for testability.
200pub trait Terminal {
201    /// Enter raw mode and alternate screen.
202    fn enter(&mut self) -> Result<(), TuiError>;
203    /// Leave alternate screen and raw mode.
204    fn leave(&mut self) -> Result<(), TuiError>;
205    /// Get terminal size (width, height).
206    fn size(&self) -> Result<(u16, u16), TuiError>;
207    /// Poll for events with timeout.
208    fn poll(&self, timeout: Duration) -> Result<bool, TuiError>;
209    /// Read the next event.
210    fn read_event(&self) -> Result<CrosstermEvent, TuiError>;
211    /// Flush output.
212    fn flush(
213        &mut self,
214        buffer: &mut CellBuffer,
215        renderer: &mut DiffRenderer,
216    ) -> Result<(), TuiError>;
217    /// Enable mouse capture.
218    fn enable_mouse(&mut self) -> Result<(), TuiError>;
219    /// Disable mouse capture.
220    fn disable_mouse(&mut self) -> Result<(), TuiError>;
221}
222
223/// Backend trait for raw terminal operations (crossterm calls).
224/// This layer exists purely for testability.
225pub trait TerminalBackend {
226    fn enable_raw_mode(&mut self) -> Result<(), TuiError>;
227    fn disable_raw_mode(&mut self) -> Result<(), TuiError>;
228    fn enter_alternate_screen(&mut self) -> Result<(), TuiError>;
229    fn leave_alternate_screen(&mut self) -> Result<(), TuiError>;
230    fn hide_cursor(&mut self) -> Result<(), TuiError>;
231    fn show_cursor(&mut self) -> Result<(), TuiError>;
232    fn size(&self) -> Result<(u16, u16), TuiError>;
233    fn poll(&self, timeout: Duration) -> Result<bool, TuiError>;
234    fn read_event(&self) -> Result<CrosstermEvent, TuiError>;
235    fn write_flush(
236        &mut self,
237        buffer: &mut CellBuffer,
238        renderer: &mut DiffRenderer,
239    ) -> Result<(), TuiError>;
240    fn enable_mouse_capture(&mut self) -> Result<(), TuiError>;
241    fn disable_mouse_capture(&mut self) -> Result<(), TuiError>;
242}
243
244/// Real crossterm backend.
245pub struct CrosstermBackend {
246    stdout: Stdout,
247}
248
249impl CrosstermBackend {
250    pub fn new() -> Self {
251        Self {
252            stdout: io::stdout(),
253        }
254    }
255}
256
257impl Default for CrosstermBackend {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263impl TerminalBackend for CrosstermBackend {
264    fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
265        enable_raw_mode()?;
266        Ok(())
267    }
268    fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
269        let _ = disable_raw_mode();
270        Ok(())
271    }
272    fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
273        execute!(self.stdout, EnterAlternateScreen)?;
274        Ok(())
275    }
276    fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
277        let _ = execute!(self.stdout, LeaveAlternateScreen);
278        Ok(())
279    }
280    fn hide_cursor(&mut self) -> Result<(), TuiError> {
281        execute!(self.stdout, cursor::Hide)?;
282        Ok(())
283    }
284    fn show_cursor(&mut self) -> Result<(), TuiError> {
285        let _ = execute!(self.stdout, cursor::Show);
286        Ok(())
287    }
288    fn size(&self) -> Result<(u16, u16), TuiError> {
289        Ok(crossterm::terminal::size()?)
290    }
291    fn poll(&self, timeout: Duration) -> Result<bool, TuiError> {
292        Ok(event::poll(timeout)?)
293    }
294    fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
295        Ok(event::read()?)
296    }
297    fn write_flush(
298        &mut self,
299        buffer: &mut CellBuffer,
300        renderer: &mut DiffRenderer,
301    ) -> Result<(), TuiError> {
302        renderer.flush(buffer, &mut self.stdout)?;
303        self.stdout.flush()?;
304        Ok(())
305    }
306    fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
307        execute!(self.stdout, crossterm::event::EnableMouseCapture)?;
308        Ok(())
309    }
310    fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
311        let _ = execute!(self.stdout, crossterm::event::DisableMouseCapture);
312        Ok(())
313    }
314}
315
316/// Testable backend with generic writer for capturing escape sequences.
317/// This backend allows testing terminal output without a real TTY.
318#[allow(clippy::struct_excessive_bools)]
319pub struct TestableBackend<W: Write> {
320    writer: W,
321    size: (u16, u16),
322    raw_mode: bool,
323    alternate_screen: bool,
324    cursor_hidden: bool,
325    mouse_captured: bool,
326    events: std::cell::RefCell<std::collections::VecDeque<CrosstermEvent>>,
327    poll_results: std::cell::RefCell<std::collections::VecDeque<bool>>,
328}
329
330impl<W: Write> TestableBackend<W> {
331    /// Create a new testable backend with the given writer and size.
332    pub fn new(writer: W, width: u16, height: u16) -> Self {
333        Self {
334            writer,
335            size: (width, height),
336            raw_mode: false,
337            alternate_screen: false,
338            cursor_hidden: false,
339            mouse_captured: false,
340            events: std::cell::RefCell::new(std::collections::VecDeque::new()),
341            poll_results: std::cell::RefCell::new(std::collections::VecDeque::new()),
342        }
343    }
344
345    /// Queue events to be returned by `read_event`.
346    pub fn with_events(self, events: Vec<CrosstermEvent>) -> Self {
347        *self.events.borrow_mut() = events.into_iter().collect();
348        self
349    }
350
351    /// Queue poll results.
352    pub fn with_polls(self, polls: Vec<bool>) -> Self {
353        *self.poll_results.borrow_mut() = polls.into_iter().collect();
354        self
355    }
356
357    /// Check if raw mode was enabled.
358    pub fn is_raw_mode(&self) -> bool {
359        self.raw_mode
360    }
361
362    /// Check if alternate screen was entered.
363    pub fn is_alternate_screen(&self) -> bool {
364        self.alternate_screen
365    }
366
367    /// Check if cursor is hidden.
368    pub fn is_cursor_hidden(&self) -> bool {
369        self.cursor_hidden
370    }
371
372    /// Check if mouse is captured.
373    pub fn is_mouse_captured(&self) -> bool {
374        self.mouse_captured
375    }
376
377    /// Get the underlying writer (consumes self).
378    pub fn into_writer(self) -> W {
379        self.writer
380    }
381}
382
383impl<W: Write> TerminalBackend for TestableBackend<W> {
384    fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
385        self.raw_mode = true;
386        Ok(())
387    }
388
389    fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
390        self.raw_mode = false;
391        Ok(())
392    }
393
394    fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
395        self.alternate_screen = true;
396        // Write the actual escape sequence for testing
397        execute!(self.writer, EnterAlternateScreen)?;
398        Ok(())
399    }
400
401    fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
402        self.alternate_screen = false;
403        let _ = execute!(self.writer, LeaveAlternateScreen);
404        Ok(())
405    }
406
407    fn hide_cursor(&mut self) -> Result<(), TuiError> {
408        self.cursor_hidden = true;
409        execute!(self.writer, cursor::Hide)?;
410        Ok(())
411    }
412
413    fn show_cursor(&mut self) -> Result<(), TuiError> {
414        self.cursor_hidden = false;
415        let _ = execute!(self.writer, cursor::Show);
416        Ok(())
417    }
418
419    fn size(&self) -> Result<(u16, u16), TuiError> {
420        Ok(self.size)
421    }
422
423    fn poll(&self, _timeout: Duration) -> Result<bool, TuiError> {
424        Ok(self.poll_results.borrow_mut().pop_front().unwrap_or(false))
425    }
426
427    fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
428        self.events
429            .borrow_mut()
430            .pop_front()
431            .ok_or_else(|| TuiError::Io(io::Error::new(io::ErrorKind::WouldBlock, "no events")))
432    }
433
434    fn write_flush(
435        &mut self,
436        buffer: &mut CellBuffer,
437        renderer: &mut DiffRenderer,
438    ) -> Result<(), TuiError> {
439        renderer.flush(buffer, &mut self.writer)?;
440        self.writer.flush()?;
441        Ok(())
442    }
443
444    fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
445        self.mouse_captured = true;
446        execute!(self.writer, crossterm::event::EnableMouseCapture)?;
447        Ok(())
448    }
449
450    fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
451        self.mouse_captured = false;
452        let _ = execute!(self.writer, crossterm::event::DisableMouseCapture);
453        Ok(())
454    }
455}
456
457/// Generic terminal implementation using a backend.
458pub struct GenericTerminal<B: TerminalBackend> {
459    backend: B,
460}
461
462impl<B: TerminalBackend> GenericTerminal<B> {
463    pub fn new(backend: B) -> Self {
464        Self { backend }
465    }
466}
467
468impl<B: TerminalBackend> Terminal for GenericTerminal<B> {
469    fn enter(&mut self) -> Result<(), TuiError> {
470        self.backend.enable_raw_mode()?;
471        self.backend.enter_alternate_screen()?;
472        self.backend.hide_cursor()?;
473        Ok(())
474    }
475
476    fn leave(&mut self) -> Result<(), TuiError> {
477        self.backend.show_cursor()?;
478        self.backend.leave_alternate_screen()?;
479        self.backend.disable_raw_mode()?;
480        Ok(())
481    }
482
483    fn size(&self) -> Result<(u16, u16), TuiError> {
484        self.backend.size()
485    }
486
487    fn poll(&self, timeout: Duration) -> Result<bool, TuiError> {
488        self.backend.poll(timeout)
489    }
490
491    fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
492        self.backend.read_event()
493    }
494
495    fn flush(
496        &mut self,
497        buffer: &mut CellBuffer,
498        renderer: &mut DiffRenderer,
499    ) -> Result<(), TuiError> {
500        self.backend.write_flush(buffer, renderer)
501    }
502
503    fn enable_mouse(&mut self) -> Result<(), TuiError> {
504        self.backend.enable_mouse_capture()
505    }
506
507    fn disable_mouse(&mut self) -> Result<(), TuiError> {
508        self.backend.disable_mouse_capture()
509    }
510}
511
512/// Convenience alias for crossterm-backed terminal.
513pub type CrosstermTerminal = GenericTerminal<CrosstermBackend>;
514
515/// Configuration for the TUI application.
516#[derive(Debug, Clone)]
517pub struct TuiConfig {
518    /// Tick rate in milliseconds for input polling.
519    pub tick_rate_ms: u64,
520    /// Enable mouse support.
521    pub enable_mouse: bool,
522    /// Color mode (auto-detected if not specified).
523    pub color_mode: Option<ColorMode>,
524    /// Skip Brick verification (DANGEROUS - for debugging only).
525    pub skip_verification: bool,
526    /// Target frame rate (used for budget calculation).
527    pub target_fps: u32,
528}
529
530impl Default for TuiConfig {
531    fn default() -> Self {
532        Self {
533            tick_rate_ms: 250,
534            enable_mouse: false,
535            color_mode: None,
536            target_fps: 60,
537            skip_verification: false,
538        }
539    }
540}
541
542impl TuiConfig {
543    /// Create a high-performance config (60fps, fast tick).
544    #[must_use]
545    pub fn high_performance() -> Self {
546        Self {
547            tick_rate_ms: 16,
548            target_fps: 60,
549            ..Default::default()
550        }
551    }
552
553    /// Create a power-saving config (30fps, slow tick).
554    #[must_use]
555    pub fn power_saving() -> Self {
556        Self {
557            tick_rate_ms: 100,
558            target_fps: 30,
559            ..Default::default()
560        }
561    }
562}
563
564/// Frame timing metrics.
565#[derive(Debug, Clone, Default)]
566pub struct FrameMetrics {
567    /// Time spent in verification phase.
568    pub verify_time: Duration,
569    /// Time spent in measure phase.
570    pub measure_time: Duration,
571    /// Time spent in layout phase.
572    pub layout_time: Duration,
573    /// Time spent in paint phase.
574    pub paint_time: Duration,
575    /// Total frame time.
576    pub total_time: Duration,
577    /// Frame number.
578    pub frame_count: u64,
579}
580
581/// Main TUI application runner.
582pub struct TuiApp<W: Widget> {
583    root: W,
584    config: TuiConfig,
585    input_handler: InputHandler,
586    metrics: FrameMetrics,
587    should_quit: bool,
588    color_mode: ColorMode,
589}
590
591/// Internal app runner that accepts a Terminal implementation.
592struct AppRunner<'a, W: Widget, T: Terminal> {
593    app: &'a mut TuiApp<W>,
594    terminal: T,
595    buffer: CellBuffer,
596    renderer: DiffRenderer,
597}
598
599impl<W: Widget, T: Terminal> AppRunner<'_, W, T> {
600    fn run_loop(&mut self) -> Result<(), TuiError> {
601        let tick_duration = Duration::from_millis(self.app.config.tick_rate_ms);
602
603        loop {
604            let frame_start = Instant::now();
605
606            // Check for terminal resize
607            let (width, height) = self.terminal.size()?;
608            if width != self.buffer.width() || height != self.buffer.height() {
609                self.buffer.resize(width, height);
610                self.renderer.reset();
611            }
612
613            // Phase 1: Verify (Jidoka gate)
614            let verify_start = Instant::now();
615            if !self.app.config.skip_verification {
616                let verification = self.app.root.verify();
617                if !verification.is_valid() {
618                    return Err(TuiError::VerificationFailed(VerificationError::from(
619                        verification,
620                    )));
621                }
622            }
623            self.app.metrics.verify_time = verify_start.elapsed();
624
625            // Phase 2: Render frame
626            self.app.render_frame(&mut self.buffer);
627
628            // Phase 3: Flush to terminal
629            self.terminal.flush(&mut self.buffer, &mut self.renderer)?;
630
631            self.app.metrics.total_time = frame_start.elapsed();
632            self.app.metrics.frame_count += 1;
633
634            // Phase 4: Handle input
635            if self.terminal.poll(tick_duration)? {
636                if let CrosstermEvent::Key(key) = self.terminal.read_event()? {
637                    if key.code == KeyCode::Char('q')
638                        || key.code == KeyCode::Char('c')
639                            && key
640                                .modifiers
641                                .contains(crossterm::event::KeyModifiers::CONTROL)
642                    {
643                        self.app.should_quit = true;
644                    }
645
646                    if let Some(event) = self.app.input_handler.convert(CrosstermEvent::Key(key)) {
647                        let _ = self.app.root.event(&event);
648                    }
649                }
650            }
651
652            if self.app.should_quit {
653                break;
654            }
655        }
656
657        Ok(())
658    }
659}
660
661impl<W: Widget> TuiApp<W> {
662    /// Create a new TUI application with the given root widget.
663    pub fn new(root: W) -> Result<Self, TuiError> {
664        // Jidoka: reject Bricks with no assertions
665        if root.assertions().is_empty() {
666            return Err(TuiError::InvalidBrick(
667                "Root widget has no assertions - every Brick must have at least one falsifiable assertion".to_string(),
668            ));
669        }
670
671        Ok(Self {
672            root,
673            config: TuiConfig::default(),
674            input_handler: InputHandler::new(),
675            metrics: FrameMetrics::default(),
676            should_quit: false,
677            color_mode: ColorMode::detect(),
678        })
679    }
680
681    /// Set the configuration.
682    #[must_use]
683    pub fn with_config(mut self, config: TuiConfig) -> Self {
684        if let Some(mode) = config.color_mode {
685            self.color_mode = mode;
686        }
687        self.config = config;
688        self
689    }
690
691    /// Set the input handler.
692    #[must_use]
693    pub fn with_input_handler(mut self, handler: InputHandler) -> Self {
694        self.input_handler = handler;
695        self
696    }
697
698    /// Get a reference to the root widget.
699    #[must_use]
700    pub fn root(&self) -> &W {
701        &self.root
702    }
703
704    /// Get a mutable reference to the root widget.
705    pub fn root_mut(&mut self) -> &mut W {
706        &mut self.root
707    }
708
709    /// Get the current frame metrics.
710    #[must_use]
711    pub fn metrics(&self) -> &FrameMetrics {
712        &self.metrics
713    }
714
715    /// Request the application to quit.
716    pub fn quit(&mut self) {
717        self.should_quit = true;
718    }
719
720    /// Run the application (blocking).
721    pub fn run(&mut self) -> Result<(), TuiError> {
722        let backend = CrosstermBackend::new();
723        let terminal = GenericTerminal::new(backend);
724        self.run_with_terminal(terminal)
725    }
726
727    /// Run the application with a custom terminal implementation.
728    /// This is the testable entry point.
729    pub fn run_with_terminal<T: Terminal>(&mut self, mut terminal: T) -> Result<(), TuiError> {
730        terminal.enter()?;
731
732        if self.config.enable_mouse {
733            terminal.enable_mouse()?;
734        }
735
736        // Get initial terminal size
737        let (width, height) = terminal.size()?;
738        let buffer = CellBuffer::new(width, height);
739        let renderer = DiffRenderer::with_color_mode(self.color_mode);
740
741        let mut runner = AppRunner {
742            app: self,
743            terminal,
744            buffer,
745            renderer,
746        };
747
748        let result = runner.run_loop();
749
750        if runner.app.config.enable_mouse {
751            runner.terminal.disable_mouse()?;
752        }
753        runner.terminal.leave()?;
754
755        result
756    }
757
758    fn render_frame(&mut self, buffer: &mut CellBuffer) {
759        let width = buffer.width();
760        let height = buffer.height();
761
762        // Phase 2a: Measure
763        let measure_start = Instant::now();
764        let constraints = Constraints::new(0.0, f32::from(width), 0.0, f32::from(height));
765        let _size = self.root.measure(constraints);
766        self.metrics.measure_time = measure_start.elapsed();
767
768        // Phase 2b: Layout
769        let layout_start = Instant::now();
770        let bounds = Rect::new(0.0, 0.0, f32::from(width), f32::from(height));
771        let _ = self.root.layout(bounds);
772        self.metrics.layout_time = layout_start.elapsed();
773
774        // Phase 2c: Paint
775        let paint_start = Instant::now();
776        {
777            let mut canvas = DirectTerminalCanvas::new(buffer);
778            self.root.paint(&mut canvas);
779        }
780        self.metrics.paint_time = paint_start.elapsed();
781    }
782}
783
784#[cfg(test)]
785mod tests {
786    use super::*;
787    use presentar_core::{
788        Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Event, LayoutResult,
789        Size, TypeId,
790    };
791    use std::any::Any;
792    use std::time::Duration;
793
794    struct TestWidget {
795        assertions: Vec<BrickAssertion>,
796    }
797
798    impl TestWidget {
799        fn new() -> Self {
800            Self {
801                assertions: vec![BrickAssertion::max_latency_ms(16)],
802            }
803        }
804
805        fn without_assertions() -> Self {
806            Self { assertions: vec![] }
807        }
808    }
809
810    impl Brick for TestWidget {
811        fn brick_name(&self) -> &'static str {
812            "test_widget"
813        }
814
815        fn assertions(&self) -> &[BrickAssertion] {
816            &self.assertions
817        }
818
819        fn budget(&self) -> BrickBudget {
820            BrickBudget::default()
821        }
822
823        fn verify(&self) -> BrickVerification {
824            BrickVerification {
825                passed: self.assertions.clone(),
826                failed: vec![],
827                verification_time: Duration::from_micros(10),
828            }
829        }
830
831        fn to_html(&self) -> String {
832            String::new()
833        }
834
835        fn to_css(&self) -> String {
836            String::new()
837        }
838    }
839
840    impl Widget for TestWidget {
841        fn type_id(&self) -> TypeId {
842            TypeId::of::<Self>()
843        }
844
845        fn measure(&self, constraints: Constraints) -> Size {
846            constraints.constrain(Size::new(10.0, 5.0))
847        }
848
849        fn layout(&mut self, bounds: Rect) -> LayoutResult {
850            LayoutResult {
851                size: Size::new(bounds.width, bounds.height),
852            }
853        }
854
855        fn paint(&self, canvas: &mut dyn Canvas) {
856            canvas.fill_rect(Rect::new(0.0, 0.0, 10.0, 5.0), Color::BLUE);
857        }
858
859        fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
860            None
861        }
862
863        fn children(&self) -> &[Box<dyn Widget>] {
864            &[]
865        }
866
867        fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
868            &mut []
869        }
870    }
871
872    #[test]
873    fn test_tui_app_creation() {
874        let widget = TestWidget::new();
875        let app = TuiApp::new(widget);
876        assert!(app.is_ok());
877    }
878
879    #[test]
880    fn test_tui_app_rejects_empty_assertions() {
881        let widget = TestWidget::without_assertions();
882        let app = TuiApp::new(widget);
883        assert!(app.is_err());
884        let err = app.err().expect("expected error");
885        assert!(matches!(err, TuiError::InvalidBrick(_)));
886    }
887
888    #[test]
889    fn test_config_default() {
890        let config = TuiConfig::default();
891        assert_eq!(config.tick_rate_ms, 250);
892        assert_eq!(config.target_fps, 60);
893        assert!(!config.enable_mouse);
894        assert!(!config.skip_verification);
895        assert!(config.color_mode.is_none());
896    }
897
898    #[test]
899    fn test_config_high_performance() {
900        let config = TuiConfig::high_performance();
901        assert_eq!(config.tick_rate_ms, 16);
902        assert_eq!(config.target_fps, 60);
903    }
904
905    #[test]
906    fn test_config_power_saving() {
907        let config = TuiConfig::power_saving();
908        assert_eq!(config.tick_rate_ms, 100);
909        assert_eq!(config.target_fps, 30);
910    }
911
912    #[test]
913    fn test_tui_app_with_config() {
914        let widget = TestWidget::new();
915        let mut app = TuiApp::new(widget).unwrap();
916
917        let config = TuiConfig {
918            tick_rate_ms: 50,
919            enable_mouse: true,
920            color_mode: Some(ColorMode::Color256),
921            skip_verification: false,
922            target_fps: 30,
923        };
924
925        app = app.with_config(config);
926        assert!(app.metrics().frame_count == 0);
927    }
928
929    #[test]
930    fn test_tui_app_with_input_handler() {
931        let widget = TestWidget::new();
932        let mut app = TuiApp::new(widget).unwrap();
933
934        let mut handler = InputHandler::new();
935        handler.add_binding(crate::input::KeyBinding::simple(
936            crossterm::event::KeyCode::Char('q'),
937            "quit",
938        ));
939
940        app = app.with_input_handler(handler);
941        assert!(app.root().assertions().len() == 1);
942    }
943
944    #[test]
945    fn test_tui_app_root_accessors() {
946        let widget = TestWidget::new();
947        let mut app = TuiApp::new(widget).unwrap();
948
949        assert_eq!(app.root().brick_name(), "test_widget");
950        assert_eq!(app.root_mut().brick_name(), "test_widget");
951    }
952
953    #[test]
954    fn test_tui_app_metrics() {
955        let widget = TestWidget::new();
956        let app = TuiApp::new(widget).unwrap();
957
958        let metrics = app.metrics();
959        assert_eq!(metrics.frame_count, 0);
960        assert_eq!(metrics.total_time, Duration::ZERO);
961    }
962
963    #[test]
964    fn test_tui_app_quit() {
965        let widget = TestWidget::new();
966        let mut app = TuiApp::new(widget).unwrap();
967
968        assert!(!app.should_quit);
969        app.quit();
970        assert!(app.should_quit);
971    }
972
973    #[test]
974    fn test_frame_metrics_default() {
975        let metrics = FrameMetrics::default();
976        assert_eq!(metrics.frame_count, 0);
977        assert_eq!(metrics.verify_time, Duration::ZERO);
978        assert_eq!(metrics.measure_time, Duration::ZERO);
979        assert_eq!(metrics.layout_time, Duration::ZERO);
980        assert_eq!(metrics.paint_time, Duration::ZERO);
981        assert_eq!(metrics.total_time, Duration::ZERO);
982    }
983
984    #[test]
985    fn test_config_with_color_mode_override() {
986        let widget = TestWidget::new();
987        let app = TuiApp::new(widget).unwrap();
988
989        let config = TuiConfig {
990            color_mode: Some(ColorMode::Mono),
991            ..Default::default()
992        };
993
994        let app = app.with_config(config);
995        assert_eq!(app.color_mode, ColorMode::Mono);
996    }
997
998    #[test]
999    fn test_config_without_color_mode() {
1000        let widget = TestWidget::new();
1001        let app = TuiApp::new(widget).unwrap();
1002        let original_mode = app.color_mode;
1003
1004        let config = TuiConfig {
1005            color_mode: None,
1006            ..Default::default()
1007        };
1008
1009        let app = app.with_config(config);
1010        assert_eq!(app.color_mode, original_mode);
1011    }
1012
1013    #[test]
1014    fn test_render_frame() {
1015        let widget = TestWidget::new();
1016        let mut app = TuiApp::new(widget).unwrap();
1017        let mut buffer = CellBuffer::new(80, 24);
1018
1019        // Render a frame and verify metrics are updated
1020        app.render_frame(&mut buffer);
1021
1022        assert!(
1023            app.metrics.measure_time > Duration::ZERO || app.metrics.measure_time == Duration::ZERO
1024        );
1025        assert!(app.metrics.layout_time >= Duration::ZERO);
1026        assert!(app.metrics.paint_time >= Duration::ZERO);
1027    }
1028
1029    #[test]
1030    fn test_render_frame_updates_metrics() {
1031        let widget = TestWidget::new();
1032        let mut app = TuiApp::new(widget).unwrap();
1033        let mut buffer = CellBuffer::new(40, 10);
1034
1035        // Render multiple frames
1036        for _ in 0..3 {
1037            app.render_frame(&mut buffer);
1038        }
1039
1040        // Metrics should be set (even if durations are very small)
1041        let metrics = app.metrics();
1042        assert_eq!(metrics.frame_count, 0); // frame_count is only updated in run_loop
1043    }
1044
1045    #[test]
1046    fn test_render_frame_with_different_buffer_sizes() {
1047        let widget = TestWidget::new();
1048        let mut app = TuiApp::new(widget).unwrap();
1049
1050        // Small buffer
1051        let mut small_buffer = CellBuffer::new(10, 5);
1052        app.render_frame(&mut small_buffer);
1053
1054        // Large buffer
1055        let mut large_buffer = CellBuffer::new(200, 50);
1056        app.render_frame(&mut large_buffer);
1057
1058        // Should not panic with any buffer size
1059    }
1060
1061    #[test]
1062    fn test_frame_metrics_clone() {
1063        let metrics = FrameMetrics {
1064            verify_time: Duration::from_millis(1),
1065            measure_time: Duration::from_millis(2),
1066            layout_time: Duration::from_millis(3),
1067            paint_time: Duration::from_millis(4),
1068            total_time: Duration::from_millis(10),
1069            frame_count: 100,
1070        };
1071
1072        let cloned = metrics.clone();
1073        assert_eq!(cloned.frame_count, 100);
1074        assert_eq!(cloned.verify_time, Duration::from_millis(1));
1075    }
1076
1077    #[test]
1078    fn test_frame_metrics_debug() {
1079        let metrics = FrameMetrics::default();
1080        let debug_str = format!("{:?}", metrics);
1081        assert!(debug_str.contains("FrameMetrics"));
1082        assert!(debug_str.contains("frame_count"));
1083    }
1084
1085    #[test]
1086    fn test_tui_config_clone() {
1087        let config = TuiConfig::high_performance();
1088        let cloned = config.clone();
1089        assert_eq!(cloned.tick_rate_ms, 16);
1090        assert_eq!(cloned.target_fps, 60);
1091    }
1092
1093    #[test]
1094    fn test_tui_config_debug() {
1095        let config = TuiConfig::default();
1096        let debug_str = format!("{:?}", config);
1097        assert!(debug_str.contains("TuiConfig"));
1098        assert!(debug_str.contains("tick_rate_ms"));
1099    }
1100
1101    // Additional tests for improved coverage
1102
1103    struct FailingWidget;
1104
1105    impl Brick for FailingWidget {
1106        fn brick_name(&self) -> &'static str {
1107            "failing_widget"
1108        }
1109
1110        fn assertions(&self) -> &[BrickAssertion] {
1111            static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
1112            ASSERTIONS
1113        }
1114
1115        fn budget(&self) -> BrickBudget {
1116            BrickBudget::default()
1117        }
1118
1119        fn verify(&self) -> BrickVerification {
1120            // This widget always fails verification
1121            BrickVerification {
1122                passed: vec![],
1123                failed: vec![(
1124                    BrickAssertion::max_latency_ms(16),
1125                    "Intentional failure".to_string(),
1126                )],
1127                verification_time: Duration::from_micros(10),
1128            }
1129        }
1130
1131        fn to_html(&self) -> String {
1132            String::new()
1133        }
1134
1135        fn to_css(&self) -> String {
1136            String::new()
1137        }
1138    }
1139
1140    impl Widget for FailingWidget {
1141        fn type_id(&self) -> TypeId {
1142            TypeId::of::<Self>()
1143        }
1144
1145        fn measure(&self, constraints: Constraints) -> Size {
1146            constraints.constrain(Size::new(10.0, 5.0))
1147        }
1148
1149        fn layout(&mut self, bounds: Rect) -> LayoutResult {
1150            LayoutResult {
1151                size: Size::new(bounds.width, bounds.height),
1152            }
1153        }
1154
1155        fn paint(&self, _canvas: &mut dyn Canvas) {}
1156
1157        fn event(&mut self, _event: &Event) -> Option<Box<dyn Any + Send>> {
1158            None
1159        }
1160
1161        fn children(&self) -> &[Box<dyn Widget>] {
1162            &[]
1163        }
1164
1165        fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
1166            &mut []
1167        }
1168    }
1169
1170    #[test]
1171    fn test_tui_app_with_failing_widget() {
1172        let widget = FailingWidget;
1173        let app = TuiApp::new(widget);
1174        // Should be Ok since we only check assertions on creation, not verify()
1175        assert!(app.is_ok());
1176    }
1177
1178    #[test]
1179    fn test_tui_config_all_fields() {
1180        let config = TuiConfig {
1181            tick_rate_ms: 100,
1182            enable_mouse: true,
1183            color_mode: Some(ColorMode::Color16),
1184            skip_verification: true,
1185            target_fps: 30,
1186        };
1187
1188        assert_eq!(config.tick_rate_ms, 100);
1189        assert!(config.enable_mouse);
1190        assert_eq!(config.color_mode, Some(ColorMode::Color16));
1191        assert!(config.skip_verification);
1192        assert_eq!(config.target_fps, 30);
1193    }
1194
1195    #[test]
1196    fn test_frame_metrics_all_fields() {
1197        let metrics = FrameMetrics {
1198            verify_time: Duration::from_millis(1),
1199            measure_time: Duration::from_millis(2),
1200            layout_time: Duration::from_millis(3),
1201            paint_time: Duration::from_millis(4),
1202            total_time: Duration::from_millis(10),
1203            frame_count: 42,
1204        };
1205
1206        assert_eq!(metrics.verify_time, Duration::from_millis(1));
1207        assert_eq!(metrics.measure_time, Duration::from_millis(2));
1208        assert_eq!(metrics.layout_time, Duration::from_millis(3));
1209        assert_eq!(metrics.paint_time, Duration::from_millis(4));
1210        assert_eq!(metrics.total_time, Duration::from_millis(10));
1211        assert_eq!(metrics.frame_count, 42);
1212    }
1213
1214    #[test]
1215    fn test_tui_app_skip_verification_config() {
1216        let widget = TestWidget::new();
1217        let app = TuiApp::new(widget).unwrap();
1218
1219        let config = TuiConfig {
1220            skip_verification: true,
1221            ..Default::default()
1222        };
1223
1224        let app = app.with_config(config);
1225        assert!(app.config.skip_verification);
1226    }
1227
1228    #[test]
1229    fn test_tui_app_enable_mouse_config() {
1230        let widget = TestWidget::new();
1231        let app = TuiApp::new(widget).unwrap();
1232
1233        let config = TuiConfig {
1234            enable_mouse: true,
1235            ..Default::default()
1236        };
1237
1238        let app = app.with_config(config);
1239        assert!(app.config.enable_mouse);
1240    }
1241
1242    #[test]
1243    fn test_render_frame_zero_size_buffer() {
1244        let widget = TestWidget::new();
1245        let mut app = TuiApp::new(widget).unwrap();
1246
1247        // Test with minimal buffer size
1248        let mut buffer = CellBuffer::new(1, 1);
1249        app.render_frame(&mut buffer);
1250        // Should not panic
1251    }
1252
1253    #[test]
1254    fn test_render_frame_metrics_populated() {
1255        let widget = TestWidget::new();
1256        let mut app = TuiApp::new(widget).unwrap();
1257        let mut buffer = CellBuffer::new(80, 24);
1258
1259        app.render_frame(&mut buffer);
1260
1261        // All timing metrics should be non-negative (possibly zero for fast operations)
1262        assert!(app.metrics.measure_time >= Duration::ZERO);
1263        assert!(app.metrics.layout_time >= Duration::ZERO);
1264        assert!(app.metrics.paint_time >= Duration::ZERO);
1265    }
1266
1267    #[test]
1268    fn test_tui_config_color_modes() {
1269        // Test all color modes
1270        for mode in [
1271            ColorMode::TrueColor,
1272            ColorMode::Color256,
1273            ColorMode::Color16,
1274            ColorMode::Mono,
1275        ] {
1276            let widget = TestWidget::new();
1277            let app = TuiApp::new(widget).unwrap();
1278
1279            let config = TuiConfig {
1280                color_mode: Some(mode),
1281                ..Default::default()
1282            };
1283
1284            let app = app.with_config(config);
1285            assert_eq!(app.color_mode, mode);
1286        }
1287    }
1288
1289    #[test]
1290    fn test_test_widget_brick_methods() {
1291        let widget = TestWidget::new();
1292
1293        assert_eq!(widget.brick_name(), "test_widget");
1294        assert!(!widget.assertions().is_empty());
1295        assert!(widget.verify().is_valid());
1296        assert!(widget.to_html().is_empty());
1297        assert!(widget.to_css().is_empty());
1298    }
1299
1300    #[test]
1301    fn test_test_widget_widget_methods() {
1302        let mut widget = TestWidget::new();
1303
1304        // measure
1305        let size = widget.measure(Constraints::loose(Size::new(100.0, 100.0)));
1306        assert!(size.width > 0.0);
1307        assert!(size.height > 0.0);
1308
1309        // layout
1310        let bounds = Rect::new(0.0, 0.0, 50.0, 25.0);
1311        let result = widget.layout(bounds);
1312        assert_eq!(result.size.width, 50.0);
1313        assert_eq!(result.size.height, 25.0);
1314
1315        // event
1316        let event = Event::KeyDown {
1317            key: presentar_core::Key::Enter,
1318        };
1319        assert!(widget.event(&event).is_none());
1320
1321        // children
1322        assert!(widget.children().is_empty());
1323        assert!(widget.children_mut().is_empty());
1324    }
1325
1326    #[test]
1327    fn test_tui_app_multiple_render_frames() {
1328        let widget = TestWidget::new();
1329        let mut app = TuiApp::new(widget).unwrap();
1330        let mut buffer = CellBuffer::new(80, 24);
1331
1332        // Render multiple frames to ensure stability
1333        for _ in 0..10 {
1334            app.render_frame(&mut buffer);
1335        }
1336
1337        // Should complete without panic
1338    }
1339
1340    // Mock terminal for testing run/run_loop
1341
1342    use std::cell::RefCell;
1343    use std::collections::VecDeque;
1344
1345    struct MockTerminal {
1346        size: (u16, u16),
1347        events: RefCell<VecDeque<CrosstermEvent>>,
1348        poll_results: RefCell<VecDeque<bool>>,
1349        entered: RefCell<bool>,
1350        left: RefCell<bool>,
1351        mouse_enabled: RefCell<bool>,
1352        flush_count: RefCell<u32>,
1353    }
1354
1355    impl MockTerminal {
1356        fn new(width: u16, height: u16) -> Self {
1357            Self {
1358                size: (width, height),
1359                events: RefCell::new(VecDeque::new()),
1360                poll_results: RefCell::new(VecDeque::new()),
1361                entered: RefCell::new(false),
1362                left: RefCell::new(false),
1363                mouse_enabled: RefCell::new(false),
1364                flush_count: RefCell::new(0),
1365            }
1366        }
1367
1368        fn with_events(mut self, events: Vec<CrosstermEvent>) -> Self {
1369            self.events = RefCell::new(events.into());
1370            self
1371        }
1372
1373        fn with_polls(mut self, polls: Vec<bool>) -> Self {
1374            self.poll_results = RefCell::new(polls.into());
1375            self
1376        }
1377    }
1378
1379    impl Terminal for MockTerminal {
1380        fn enter(&mut self) -> Result<(), TuiError> {
1381            *self.entered.borrow_mut() = true;
1382            Ok(())
1383        }
1384
1385        fn leave(&mut self) -> Result<(), TuiError> {
1386            *self.left.borrow_mut() = true;
1387            Ok(())
1388        }
1389
1390        fn size(&self) -> Result<(u16, u16), TuiError> {
1391            Ok(self.size)
1392        }
1393
1394        fn poll(&self, _timeout: Duration) -> Result<bool, TuiError> {
1395            Ok(self.poll_results.borrow_mut().pop_front().unwrap_or(false))
1396        }
1397
1398        fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
1399            self.events
1400                .borrow_mut()
1401                .pop_front()
1402                .ok_or_else(|| TuiError::Io(io::Error::new(io::ErrorKind::Other, "no event")))
1403        }
1404
1405        fn flush(
1406            &mut self,
1407            _buffer: &mut CellBuffer,
1408            _renderer: &mut DiffRenderer,
1409        ) -> Result<(), TuiError> {
1410            *self.flush_count.borrow_mut() += 1;
1411            Ok(())
1412        }
1413
1414        fn enable_mouse(&mut self) -> Result<(), TuiError> {
1415            *self.mouse_enabled.borrow_mut() = true;
1416            Ok(())
1417        }
1418
1419        fn disable_mouse(&mut self) -> Result<(), TuiError> {
1420            *self.mouse_enabled.borrow_mut() = false;
1421            Ok(())
1422        }
1423    }
1424
1425    #[test]
1426    fn test_run_with_terminal_quit_on_q() {
1427        let widget = TestWidget::new();
1428        let mut app = TuiApp::new(widget).unwrap();
1429
1430        let terminal = MockTerminal::new(80, 24)
1431            .with_polls(vec![true])
1432            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1433                KeyCode::Char('q'),
1434                crossterm::event::KeyModifiers::NONE,
1435            ))]);
1436
1437        let result = app.run_with_terminal(terminal);
1438        assert!(result.is_ok());
1439        assert!(app.should_quit);
1440    }
1441
1442    #[test]
1443    fn test_run_with_terminal_ctrl_c() {
1444        let widget = TestWidget::new();
1445        let mut app = TuiApp::new(widget).unwrap();
1446
1447        let terminal = MockTerminal::new(80, 24)
1448            .with_polls(vec![true])
1449            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1450                KeyCode::Char('c'),
1451                crossterm::event::KeyModifiers::CONTROL,
1452            ))]);
1453
1454        let result = app.run_with_terminal(terminal);
1455        assert!(result.is_ok());
1456        assert!(app.should_quit);
1457    }
1458
1459    #[test]
1460    fn test_run_with_terminal_mouse_enabled() {
1461        let widget = TestWidget::new();
1462        let mut app = TuiApp::new(widget).unwrap();
1463        app.config.enable_mouse = true;
1464
1465        let terminal = MockTerminal::new(80, 24)
1466            .with_polls(vec![true])
1467            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1468                KeyCode::Char('q'),
1469                crossterm::event::KeyModifiers::NONE,
1470            ))]);
1471
1472        let result = app.run_with_terminal(terminal);
1473        assert!(result.is_ok());
1474    }
1475
1476    #[test]
1477    fn test_run_with_terminal_skip_verification() {
1478        let widget = FailingWidget;
1479        let mut app = TuiApp::new(widget).unwrap();
1480        app.config.skip_verification = true;
1481
1482        let terminal = MockTerminal::new(80, 24)
1483            .with_polls(vec![true])
1484            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1485                KeyCode::Char('q'),
1486                crossterm::event::KeyModifiers::NONE,
1487            ))]);
1488
1489        // Should succeed because verification is skipped
1490        let result = app.run_with_terminal(terminal);
1491        assert!(result.is_ok());
1492    }
1493
1494    #[test]
1495    fn test_run_with_terminal_verification_failure() {
1496        let widget = FailingWidget;
1497        let mut app = TuiApp::new(widget).unwrap();
1498
1499        let terminal = MockTerminal::new(80, 24).with_polls(vec![false]);
1500
1501        // Should fail verification
1502        let result = app.run_with_terminal(terminal);
1503        assert!(result.is_err());
1504        assert!(matches!(result, Err(TuiError::VerificationFailed(_))));
1505    }
1506
1507    #[test]
1508    fn test_run_with_terminal_no_events() {
1509        let widget = TestWidget::new();
1510        let mut app = TuiApp::new(widget).unwrap();
1511        app.quit(); // Pre-set quit to exit immediately
1512
1513        let terminal = MockTerminal::new(80, 24).with_polls(vec![false]);
1514
1515        let result = app.run_with_terminal(terminal);
1516        assert!(result.is_ok());
1517    }
1518
1519    #[test]
1520    fn test_run_with_terminal_other_key() {
1521        let widget = TestWidget::new();
1522        let mut app = TuiApp::new(widget).unwrap();
1523
1524        let terminal = MockTerminal::new(80, 24)
1525            .with_polls(vec![true, true])
1526            .with_events(vec![
1527                CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1528                    KeyCode::Enter,
1529                    crossterm::event::KeyModifiers::NONE,
1530                )),
1531                CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1532                    KeyCode::Char('q'),
1533                    crossterm::event::KeyModifiers::NONE,
1534                )),
1535            ]);
1536
1537        let result = app.run_with_terminal(terminal);
1538        assert!(result.is_ok());
1539    }
1540
1541    #[test]
1542    fn test_run_with_terminal_frame_count() {
1543        let widget = TestWidget::new();
1544        let mut app = TuiApp::new(widget).unwrap();
1545
1546        let terminal = MockTerminal::new(80, 24)
1547            .with_polls(vec![false, false, true])
1548            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1549                KeyCode::Char('q'),
1550                crossterm::event::KeyModifiers::NONE,
1551            ))]);
1552
1553        let result = app.run_with_terminal(terminal);
1554        assert!(result.is_ok());
1555        assert!(app.metrics.frame_count >= 1);
1556    }
1557
1558    #[test]
1559    fn test_crossterm_backend_new() {
1560        let backend = CrosstermBackend::new();
1561        // Just verify it can be created
1562        let _ = backend;
1563    }
1564
1565    #[test]
1566    fn test_crossterm_backend_default() {
1567        let backend = CrosstermBackend::default();
1568        let _ = backend;
1569    }
1570
1571    // Mock backend for testing GenericTerminal
1572    struct MockBackend {
1573        size: (u16, u16),
1574        events: RefCell<VecDeque<CrosstermEvent>>,
1575        poll_results: RefCell<VecDeque<bool>>,
1576        raw_mode: RefCell<bool>,
1577        alternate_screen: RefCell<bool>,
1578        cursor_hidden: RefCell<bool>,
1579        mouse_captured: RefCell<bool>,
1580    }
1581
1582    impl MockBackend {
1583        fn new(width: u16, height: u16) -> Self {
1584            Self {
1585                size: (width, height),
1586                events: RefCell::new(VecDeque::new()),
1587                poll_results: RefCell::new(VecDeque::new()),
1588                raw_mode: RefCell::new(false),
1589                alternate_screen: RefCell::new(false),
1590                cursor_hidden: RefCell::new(false),
1591                mouse_captured: RefCell::new(false),
1592            }
1593        }
1594
1595        fn with_events(self, events: Vec<CrosstermEvent>) -> Self {
1596            *self.events.borrow_mut() = events.into();
1597            self
1598        }
1599
1600        fn with_polls(self, polls: Vec<bool>) -> Self {
1601            *self.poll_results.borrow_mut() = polls.into();
1602            self
1603        }
1604    }
1605
1606    impl TerminalBackend for MockBackend {
1607        fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
1608            *self.raw_mode.borrow_mut() = true;
1609            Ok(())
1610        }
1611        fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
1612            *self.raw_mode.borrow_mut() = false;
1613            Ok(())
1614        }
1615        fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
1616            *self.alternate_screen.borrow_mut() = true;
1617            Ok(())
1618        }
1619        fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
1620            *self.alternate_screen.borrow_mut() = false;
1621            Ok(())
1622        }
1623        fn hide_cursor(&mut self) -> Result<(), TuiError> {
1624            *self.cursor_hidden.borrow_mut() = true;
1625            Ok(())
1626        }
1627        fn show_cursor(&mut self) -> Result<(), TuiError> {
1628            *self.cursor_hidden.borrow_mut() = false;
1629            Ok(())
1630        }
1631        fn size(&self) -> Result<(u16, u16), TuiError> {
1632            Ok(self.size)
1633        }
1634        fn poll(&self, _timeout: Duration) -> Result<bool, TuiError> {
1635            Ok(self.poll_results.borrow_mut().pop_front().unwrap_or(false))
1636        }
1637        fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
1638            self.events
1639                .borrow_mut()
1640                .pop_front()
1641                .ok_or_else(|| TuiError::Io(io::Error::new(io::ErrorKind::Other, "no event")))
1642        }
1643        fn write_flush(
1644            &mut self,
1645            _buffer: &mut CellBuffer,
1646            _renderer: &mut DiffRenderer,
1647        ) -> Result<(), TuiError> {
1648            Ok(())
1649        }
1650        fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
1651            *self.mouse_captured.borrow_mut() = true;
1652            Ok(())
1653        }
1654        fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
1655            *self.mouse_captured.borrow_mut() = false;
1656            Ok(())
1657        }
1658    }
1659
1660    #[test]
1661    fn test_generic_terminal_enter_leave() {
1662        let backend = MockBackend::new(80, 24);
1663        let mut terminal = GenericTerminal::new(backend);
1664
1665        terminal.enter().unwrap();
1666        assert!(*terminal.backend.raw_mode.borrow());
1667        assert!(*terminal.backend.alternate_screen.borrow());
1668        assert!(*terminal.backend.cursor_hidden.borrow());
1669
1670        terminal.leave().unwrap();
1671        assert!(!*terminal.backend.raw_mode.borrow());
1672        assert!(!*terminal.backend.alternate_screen.borrow());
1673        assert!(!*terminal.backend.cursor_hidden.borrow());
1674    }
1675
1676    #[test]
1677    fn test_generic_terminal_size() {
1678        let backend = MockBackend::new(100, 50);
1679        let terminal = GenericTerminal::new(backend);
1680        let (w, h) = terminal.size().unwrap();
1681        assert_eq!(w, 100);
1682        assert_eq!(h, 50);
1683    }
1684
1685    #[test]
1686    fn test_generic_terminal_poll_read() {
1687        let backend = MockBackend::new(80, 24)
1688            .with_polls(vec![true, false])
1689            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1690                KeyCode::Enter,
1691                crossterm::event::KeyModifiers::NONE,
1692            ))]);
1693        let terminal = GenericTerminal::new(backend);
1694
1695        assert!(terminal.poll(Duration::from_millis(10)).unwrap());
1696        let event = terminal.read_event().unwrap();
1697        assert!(matches!(event, CrosstermEvent::Key(_)));
1698
1699        assert!(!terminal.poll(Duration::from_millis(10)).unwrap());
1700    }
1701
1702    #[test]
1703    fn test_generic_terminal_mouse() {
1704        let backend = MockBackend::new(80, 24);
1705        let mut terminal = GenericTerminal::new(backend);
1706
1707        assert!(!*terminal.backend.mouse_captured.borrow());
1708        terminal.enable_mouse().unwrap();
1709        assert!(*terminal.backend.mouse_captured.borrow());
1710        terminal.disable_mouse().unwrap();
1711        assert!(!*terminal.backend.mouse_captured.borrow());
1712    }
1713
1714    #[test]
1715    fn test_generic_terminal_flush() {
1716        let backend = MockBackend::new(80, 24);
1717        let mut terminal = GenericTerminal::new(backend);
1718        let mut buffer = CellBuffer::new(80, 24);
1719        let mut renderer = DiffRenderer::new();
1720
1721        terminal.flush(&mut buffer, &mut renderer).unwrap();
1722    }
1723
1724    #[test]
1725    fn test_run_with_generic_terminal() {
1726        let widget = TestWidget::new();
1727        let mut app = TuiApp::new(widget).unwrap();
1728
1729        let backend = MockBackend::new(80, 24)
1730            .with_polls(vec![true])
1731            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1732                KeyCode::Char('q'),
1733                crossterm::event::KeyModifiers::NONE,
1734            ))]);
1735        let terminal = GenericTerminal::new(backend);
1736
1737        let result = app.run_with_terminal(terminal);
1738        assert!(result.is_ok());
1739        assert!(app.should_quit);
1740    }
1741
1742    #[test]
1743    fn test_mock_terminal_enter_leave() {
1744        let mut terminal = MockTerminal::new(80, 24);
1745        assert!(!*terminal.entered.borrow());
1746        terminal.enter().unwrap();
1747        assert!(*terminal.entered.borrow());
1748
1749        assert!(!*terminal.left.borrow());
1750        terminal.leave().unwrap();
1751        assert!(*terminal.left.borrow());
1752    }
1753
1754    #[test]
1755    fn test_mock_terminal_mouse() {
1756        let mut terminal = MockTerminal::new(80, 24);
1757        assert!(!*terminal.mouse_enabled.borrow());
1758        terminal.enable_mouse().unwrap();
1759        assert!(*terminal.mouse_enabled.borrow());
1760        terminal.disable_mouse().unwrap();
1761        assert!(!*terminal.mouse_enabled.borrow());
1762    }
1763
1764    #[test]
1765    fn test_mock_terminal_size() {
1766        let terminal = MockTerminal::new(100, 50);
1767        let (w, h) = terminal.size().unwrap();
1768        assert_eq!(w, 100);
1769        assert_eq!(h, 50);
1770    }
1771
1772    #[test]
1773    fn test_run_with_terminal_resize() {
1774        let widget = TestWidget::new();
1775        let mut app = TuiApp::new(widget).unwrap();
1776
1777        // Create terminal that simulates a size change by having different initial size
1778        let terminal = MockTerminal::new(40, 12)
1779            .with_polls(vec![true])
1780            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1781                KeyCode::Char('q'),
1782                crossterm::event::KeyModifiers::NONE,
1783            ))]);
1784
1785        let result = app.run_with_terminal(terminal);
1786        assert!(result.is_ok());
1787    }
1788
1789    #[test]
1790    fn test_run_with_terminal_mouse_event() {
1791        let widget = TestWidget::new();
1792        let mut app = TuiApp::new(widget).unwrap();
1793
1794        let terminal = MockTerminal::new(80, 24)
1795            .with_polls(vec![true, true])
1796            .with_events(vec![
1797                CrosstermEvent::Mouse(crossterm::event::MouseEvent {
1798                    kind: crossterm::event::MouseEventKind::Down(
1799                        crossterm::event::MouseButton::Left,
1800                    ),
1801                    column: 10,
1802                    row: 5,
1803                    modifiers: crossterm::event::KeyModifiers::NONE,
1804                }),
1805                CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1806                    KeyCode::Char('q'),
1807                    crossterm::event::KeyModifiers::NONE,
1808                )),
1809            ]);
1810
1811        let result = app.run_with_terminal(terminal);
1812        assert!(result.is_ok());
1813    }
1814
1815    #[test]
1816    fn test_run_with_terminal_non_key_event_then_quit() {
1817        let widget = TestWidget::new();
1818        let mut app = TuiApp::new(widget).unwrap();
1819
1820        let terminal = MockTerminal::new(80, 24)
1821            .with_polls(vec![true, true])
1822            .with_events(vec![
1823                CrosstermEvent::Resize(100, 50),
1824                CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1825                    KeyCode::Char('q'),
1826                    crossterm::event::KeyModifiers::NONE,
1827                )),
1828            ]);
1829
1830        let result = app.run_with_terminal(terminal);
1831        assert!(result.is_ok());
1832    }
1833
1834    #[test]
1835    fn test_app_runner_metrics_update() {
1836        let widget = TestWidget::new();
1837        let mut app = TuiApp::new(widget).unwrap();
1838
1839        let terminal = MockTerminal::new(80, 24)
1840            .with_polls(vec![false, true])
1841            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
1842                KeyCode::Char('q'),
1843                crossterm::event::KeyModifiers::NONE,
1844            ))]);
1845
1846        app.run_with_terminal(terminal).unwrap();
1847
1848        // Metrics should be populated
1849        assert!(app.metrics.frame_count >= 1);
1850    }
1851
1852    // =====================================================
1853    // TestableBackend tests - TTY mocking with escape sequences
1854    // =====================================================
1855
1856    #[test]
1857    fn test_testable_backend_new() {
1858        let buf: Vec<u8> = Vec::new();
1859        let backend = TestableBackend::new(buf, 80, 24);
1860        assert_eq!(backend.size, (80, 24));
1861        assert!(!backend.raw_mode);
1862        assert!(!backend.alternate_screen);
1863        assert!(!backend.cursor_hidden);
1864        assert!(!backend.mouse_captured);
1865    }
1866
1867    #[test]
1868    fn test_testable_backend_with_events() {
1869        let buf: Vec<u8> = Vec::new();
1870        let backend = TestableBackend::new(buf, 80, 24).with_events(vec![CrosstermEvent::Key(
1871            crossterm::event::KeyEvent::new(
1872                KeyCode::Char('a'),
1873                crossterm::event::KeyModifiers::NONE,
1874            ),
1875        )]);
1876        assert_eq!(backend.events.borrow().len(), 1);
1877    }
1878
1879    #[test]
1880    fn test_testable_backend_with_polls() {
1881        let buf: Vec<u8> = Vec::new();
1882        let backend = TestableBackend::new(buf, 80, 24).with_polls(vec![true, false, true]);
1883        assert_eq!(backend.poll_results.borrow().len(), 3);
1884    }
1885
1886    #[test]
1887    fn test_testable_backend_enable_raw_mode() {
1888        let buf: Vec<u8> = Vec::new();
1889        let mut backend = TestableBackend::new(buf, 80, 24);
1890        assert!(!backend.is_raw_mode());
1891        backend.enable_raw_mode().unwrap();
1892        assert!(backend.is_raw_mode());
1893    }
1894
1895    #[test]
1896    fn test_testable_backend_disable_raw_mode() {
1897        let buf: Vec<u8> = Vec::new();
1898        let mut backend = TestableBackend::new(buf, 80, 24);
1899        backend.enable_raw_mode().unwrap();
1900        assert!(backend.is_raw_mode());
1901        backend.disable_raw_mode().unwrap();
1902        assert!(!backend.is_raw_mode());
1903    }
1904
1905    #[test]
1906    fn test_testable_backend_enter_alternate_screen() {
1907        let buf: Vec<u8> = Vec::new();
1908        let mut backend = TestableBackend::new(buf, 80, 24);
1909        assert!(!backend.is_alternate_screen());
1910        backend.enter_alternate_screen().unwrap();
1911        assert!(backend.is_alternate_screen());
1912        // Verify escape sequence was written
1913        let output = backend.into_writer();
1914        assert!(!output.is_empty());
1915        // EnterAlternateScreen is \x1b[?1049h
1916        assert!(output.starts_with(b"\x1b["));
1917    }
1918
1919    #[test]
1920    fn test_testable_backend_leave_alternate_screen() {
1921        let buf: Vec<u8> = Vec::new();
1922        let mut backend = TestableBackend::new(buf, 80, 24);
1923        backend.enter_alternate_screen().unwrap();
1924        backend.leave_alternate_screen().unwrap();
1925        assert!(!backend.is_alternate_screen());
1926    }
1927
1928    #[test]
1929    fn test_testable_backend_hide_cursor() {
1930        let buf: Vec<u8> = Vec::new();
1931        let mut backend = TestableBackend::new(buf, 80, 24);
1932        assert!(!backend.is_cursor_hidden());
1933        backend.hide_cursor().unwrap();
1934        assert!(backend.is_cursor_hidden());
1935        // Verify escape sequence was written
1936        let output = backend.into_writer();
1937        assert!(!output.is_empty());
1938    }
1939
1940    #[test]
1941    fn test_testable_backend_show_cursor() {
1942        let buf: Vec<u8> = Vec::new();
1943        let mut backend = TestableBackend::new(buf, 80, 24);
1944        backend.hide_cursor().unwrap();
1945        backend.show_cursor().unwrap();
1946        assert!(!backend.is_cursor_hidden());
1947    }
1948
1949    #[test]
1950    fn test_testable_backend_size() {
1951        let buf: Vec<u8> = Vec::new();
1952        let backend = TestableBackend::new(buf, 120, 40);
1953        assert_eq!(backend.size().unwrap(), (120, 40));
1954    }
1955
1956    #[test]
1957    fn test_testable_backend_poll() {
1958        let buf: Vec<u8> = Vec::new();
1959        let backend = TestableBackend::new(buf, 80, 24).with_polls(vec![true, false]);
1960        assert!(backend.poll(Duration::from_millis(100)).unwrap());
1961        assert!(!backend.poll(Duration::from_millis(100)).unwrap());
1962        // Default when empty
1963        assert!(!backend.poll(Duration::from_millis(100)).unwrap());
1964    }
1965
1966    #[test]
1967    fn test_testable_backend_read_event() {
1968        let buf: Vec<u8> = Vec::new();
1969        let backend = TestableBackend::new(buf, 80, 24).with_events(vec![CrosstermEvent::Key(
1970            crossterm::event::KeyEvent::new(
1971                KeyCode::Char('x'),
1972                crossterm::event::KeyModifiers::NONE,
1973            ),
1974        )]);
1975        let event = backend.read_event().unwrap();
1976        assert!(matches!(event, CrosstermEvent::Key(_)));
1977    }
1978
1979    #[test]
1980    fn test_testable_backend_read_event_empty() {
1981        let buf: Vec<u8> = Vec::new();
1982        let backend = TestableBackend::new(buf, 80, 24);
1983        let result = backend.read_event();
1984        assert!(result.is_err());
1985    }
1986
1987    #[test]
1988    fn test_testable_backend_enable_mouse_capture() {
1989        let buf: Vec<u8> = Vec::new();
1990        let mut backend = TestableBackend::new(buf, 80, 24);
1991        assert!(!backend.is_mouse_captured());
1992        backend.enable_mouse_capture().unwrap();
1993        assert!(backend.is_mouse_captured());
1994        // Verify escape sequence was written
1995        let output = backend.into_writer();
1996        assert!(!output.is_empty());
1997    }
1998
1999    #[test]
2000    fn test_testable_backend_disable_mouse_capture() {
2001        let buf: Vec<u8> = Vec::new();
2002        let mut backend = TestableBackend::new(buf, 80, 24);
2003        backend.enable_mouse_capture().unwrap();
2004        backend.disable_mouse_capture().unwrap();
2005        assert!(!backend.is_mouse_captured());
2006    }
2007
2008    #[test]
2009    fn test_testable_backend_write_flush() {
2010        use crate::direct::Cell;
2011
2012        let buf: Vec<u8> = Vec::new();
2013        let mut backend = TestableBackend::new(buf, 80, 24);
2014        let mut buffer = CellBuffer::new(80, 24);
2015        let mut renderer = DiffRenderer::new();
2016
2017        // Write something to the buffer using the Cell API
2018        let mut cell_a = Cell::default();
2019        cell_a.update(
2020            "A",
2021            presentar_core::Color::WHITE,
2022            presentar_core::Color::BLACK,
2023            crate::direct::Modifiers::empty(),
2024        );
2025        buffer.set(0, 0, cell_a);
2026
2027        let mut cell_b = Cell::default();
2028        cell_b.update(
2029            "B",
2030            presentar_core::Color::WHITE,
2031            presentar_core::Color::BLACK,
2032            crate::direct::Modifiers::empty(),
2033        );
2034        buffer.set(1, 0, cell_b);
2035
2036        buffer.mark_all_dirty();
2037        backend.write_flush(&mut buffer, &mut renderer).unwrap();
2038
2039        // Verify output was written
2040        let output = backend.into_writer();
2041        assert!(!output.is_empty());
2042    }
2043
2044    #[test]
2045    fn test_testable_backend_full_lifecycle() {
2046        let buf: Vec<u8> = Vec::new();
2047        let mut backend = TestableBackend::new(buf, 80, 24);
2048
2049        // Enter
2050        backend.enable_raw_mode().unwrap();
2051        backend.enter_alternate_screen().unwrap();
2052        backend.hide_cursor().unwrap();
2053
2054        assert!(backend.is_raw_mode());
2055        assert!(backend.is_alternate_screen());
2056        assert!(backend.is_cursor_hidden());
2057
2058        // Leave
2059        backend.show_cursor().unwrap();
2060        backend.leave_alternate_screen().unwrap();
2061        backend.disable_raw_mode().unwrap();
2062
2063        assert!(!backend.is_raw_mode());
2064        assert!(!backend.is_alternate_screen());
2065        assert!(!backend.is_cursor_hidden());
2066    }
2067
2068    #[test]
2069    fn test_testable_backend_escape_sequences() {
2070        let buf: Vec<u8> = Vec::new();
2071        let mut backend = TestableBackend::new(buf, 80, 24);
2072
2073        backend.enter_alternate_screen().unwrap();
2074        backend.hide_cursor().unwrap();
2075        backend.enable_mouse_capture().unwrap();
2076
2077        let output = backend.into_writer();
2078        let output_str = String::from_utf8_lossy(&output);
2079
2080        // Check for ANSI escape sequences (CSI = \x1b[)
2081        assert!(
2082            output_str.contains("\x1b["),
2083            "Expected ANSI escape sequences"
2084        );
2085    }
2086
2087    #[test]
2088    fn test_generic_terminal_with_testable_backend() {
2089        let buf: Vec<u8> = Vec::new();
2090        let backend = TestableBackend::new(buf, 80, 24)
2091            .with_polls(vec![true])
2092            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
2093                KeyCode::Char('q'),
2094                crossterm::event::KeyModifiers::NONE,
2095            ))]);
2096
2097        let mut terminal = GenericTerminal::new(backend);
2098
2099        terminal.enter().unwrap();
2100        assert_eq!(terminal.size().unwrap(), (80, 24));
2101
2102        // Poll and read
2103        assert!(terminal.poll(Duration::from_millis(10)).unwrap());
2104        let event = terminal.read_event().unwrap();
2105        assert!(matches!(event, CrosstermEvent::Key(_)));
2106
2107        terminal.leave().unwrap();
2108    }
2109
2110    #[test]
2111    fn test_testable_backend_with_tui_app() {
2112        let widget = TestWidget::new();
2113        let mut app = TuiApp::new(widget).unwrap();
2114
2115        let buf: Vec<u8> = Vec::new();
2116        let backend = TestableBackend::new(buf, 80, 24)
2117            .with_polls(vec![true])
2118            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
2119                KeyCode::Char('q'),
2120                crossterm::event::KeyModifiers::NONE,
2121            ))]);
2122
2123        let terminal = GenericTerminal::new(backend);
2124        let result = app.run_with_terminal(terminal);
2125        assert!(result.is_ok());
2126    }
2127
2128    #[test]
2129    fn test_testable_backend_captures_render_output() {
2130        let widget = TestWidget::new();
2131        let _app = TuiApp::new(widget).unwrap();
2132
2133        let buf: Vec<u8> = Vec::new();
2134        let backend = TestableBackend::new(buf, 40, 10)
2135            .with_polls(vec![true])
2136            .with_events(vec![CrosstermEvent::Key(crossterm::event::KeyEvent::new(
2137                KeyCode::Char('q'),
2138                crossterm::event::KeyModifiers::NONE,
2139            ))]);
2140
2141        let mut terminal = GenericTerminal::new(backend);
2142        terminal.enter().unwrap();
2143
2144        // Get terminal size
2145        let (width, height) = terminal.size().unwrap();
2146        assert_eq!((width, height), (40, 10));
2147
2148        terminal.leave().unwrap();
2149    }
2150}