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        contract_pre_render!();
164        self.render_times_us.push(duration.as_micros() as u64);
165    }
166
167    /// Format timing report for stderr output.
168    #[must_use]
169    pub fn format_report(&self) -> String {
170        let avg = |v: &[u64]| {
171            if v.is_empty() {
172                0
173            } else {
174                v.iter().sum::<u64>() / v.len() as u64
175            }
176        };
177        let max = |v: &[u64]| v.iter().max().copied().unwrap_or(0);
178
179        format!(
180            "[QA] input: avg={}us max={}us | lock: avg={}us max={}us | render: avg={}us max={}us | collect: {}us",
181            avg(&self.input_times_us), max(&self.input_times_us),
182            avg(&self.lock_times_us), max(&self.lock_times_us),
183            avg(&self.render_times_us), max(&self.render_times_us),
184            self.last_collect_us
185        )
186    }
187
188    /// Clear accumulated timing data.
189    pub fn clear(&mut self) {
190        self.input_times_us.clear();
191        self.lock_times_us.clear();
192        self.render_times_us.clear();
193    }
194}
195
196// =============================================================================
197// Terminal Abstraction
198// =============================================================================
199
200/// Terminal abstraction for testability.
201pub trait Terminal {
202    /// Enter raw mode and alternate screen.
203    fn enter(&mut self) -> Result<(), TuiError>;
204    /// Leave alternate screen and raw mode.
205    fn leave(&mut self) -> Result<(), TuiError>;
206    /// Get terminal size (width, height).
207    fn size(&self) -> Result<(u16, u16), TuiError>;
208    /// Poll for events with timeout.
209    fn poll(&self, timeout: Duration) -> Result<bool, TuiError>;
210    /// Read the next event.
211    fn read_event(&self) -> Result<CrosstermEvent, TuiError>;
212    /// Flush output.
213    fn flush(
214        &mut self,
215        buffer: &mut CellBuffer,
216        renderer: &mut DiffRenderer,
217    ) -> Result<(), TuiError>;
218    /// Enable mouse capture.
219    fn enable_mouse(&mut self) -> Result<(), TuiError>;
220    /// Disable mouse capture.
221    fn disable_mouse(&mut self) -> Result<(), TuiError>;
222}
223
224/// Backend trait for raw terminal operations (crossterm calls).
225/// This layer exists purely for testability.
226pub trait TerminalBackend {
227    fn enable_raw_mode(&mut self) -> Result<(), TuiError>;
228    fn disable_raw_mode(&mut self) -> Result<(), TuiError>;
229    fn enter_alternate_screen(&mut self) -> Result<(), TuiError>;
230    fn leave_alternate_screen(&mut self) -> Result<(), TuiError>;
231    fn hide_cursor(&mut self) -> Result<(), TuiError>;
232    fn show_cursor(&mut self) -> Result<(), TuiError>;
233    fn size(&self) -> Result<(u16, u16), TuiError>;
234    fn poll(&self, timeout: Duration) -> Result<bool, TuiError>;
235    fn read_event(&self) -> Result<CrosstermEvent, TuiError>;
236    fn write_flush(
237        &mut self,
238        buffer: &mut CellBuffer,
239        renderer: &mut DiffRenderer,
240    ) -> Result<(), TuiError>;
241    fn enable_mouse_capture(&mut self) -> Result<(), TuiError>;
242    fn disable_mouse_capture(&mut self) -> Result<(), TuiError>;
243}
244
245/// Real crossterm backend.
246pub struct CrosstermBackend {
247    stdout: Stdout,
248}
249
250impl CrosstermBackend {
251    pub fn new() -> Self {
252        Self {
253            stdout: io::stdout(),
254        }
255    }
256}
257
258impl Default for CrosstermBackend {
259    fn default() -> Self {
260        Self::new()
261    }
262}
263
264impl TerminalBackend for CrosstermBackend {
265    fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
266        enable_raw_mode()?;
267        Ok(())
268    }
269    fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
270        let _ = disable_raw_mode();
271        Ok(())
272    }
273    fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
274        execute!(self.stdout, EnterAlternateScreen)?;
275        Ok(())
276    }
277    fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
278        let _ = execute!(self.stdout, LeaveAlternateScreen);
279        Ok(())
280    }
281    fn hide_cursor(&mut self) -> Result<(), TuiError> {
282        execute!(self.stdout, cursor::Hide)?;
283        Ok(())
284    }
285    fn show_cursor(&mut self) -> Result<(), TuiError> {
286        let _ = execute!(self.stdout, cursor::Show);
287        Ok(())
288    }
289    fn size(&self) -> Result<(u16, u16), TuiError> {
290        Ok(crossterm::terminal::size()?)
291    }
292    fn poll(&self, timeout: Duration) -> Result<bool, TuiError> {
293        Ok(event::poll(timeout)?)
294    }
295    fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
296        Ok(event::read()?)
297    }
298    fn write_flush(
299        &mut self,
300        buffer: &mut CellBuffer,
301        renderer: &mut DiffRenderer,
302    ) -> Result<(), TuiError> {
303        renderer.flush(buffer, &mut self.stdout)?;
304        self.stdout.flush()?;
305        Ok(())
306    }
307    fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
308        execute!(self.stdout, crossterm::event::EnableMouseCapture)?;
309        Ok(())
310    }
311    fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
312        let _ = execute!(self.stdout, crossterm::event::DisableMouseCapture);
313        Ok(())
314    }
315}
316
317/// Testable backend with generic writer for capturing escape sequences.
318/// This backend allows testing terminal output without a real TTY.
319#[allow(clippy::struct_excessive_bools)]
320pub struct TestableBackend<W: Write> {
321    writer: W,
322    size: (u16, u16),
323    raw_mode: bool,
324    alternate_screen: bool,
325    cursor_hidden: bool,
326    mouse_captured: bool,
327    events: std::cell::RefCell<std::collections::VecDeque<CrosstermEvent>>,
328    poll_results: std::cell::RefCell<std::collections::VecDeque<bool>>,
329}
330
331impl<W: Write> TestableBackend<W> {
332    /// Create a new testable backend with the given writer and size.
333    pub fn new(writer: W, width: u16, height: u16) -> Self {
334        Self {
335            writer,
336            size: (width, height),
337            raw_mode: false,
338            alternate_screen: false,
339            cursor_hidden: false,
340            mouse_captured: false,
341            events: std::cell::RefCell::new(std::collections::VecDeque::new()),
342            poll_results: std::cell::RefCell::new(std::collections::VecDeque::new()),
343        }
344    }
345
346    /// Queue events to be returned by `read_event`.
347    pub fn with_events(self, events: Vec<CrosstermEvent>) -> Self {
348        *self.events.borrow_mut() = events.into_iter().collect();
349        self
350    }
351
352    /// Queue poll results.
353    pub fn with_polls(self, polls: Vec<bool>) -> Self {
354        *self.poll_results.borrow_mut() = polls.into_iter().collect();
355        self
356    }
357
358    /// Check if raw mode was enabled.
359    pub fn is_raw_mode(&self) -> bool {
360        self.raw_mode
361    }
362
363    /// Check if alternate screen was entered.
364    pub fn is_alternate_screen(&self) -> bool {
365        self.alternate_screen
366    }
367
368    /// Check if cursor is hidden.
369    pub fn is_cursor_hidden(&self) -> bool {
370        self.cursor_hidden
371    }
372
373    /// Check if mouse is captured.
374    pub fn is_mouse_captured(&self) -> bool {
375        self.mouse_captured
376    }
377
378    /// Get the underlying writer (consumes self).
379    pub fn into_writer(self) -> W {
380        self.writer
381    }
382}
383
384impl<W: Write> TerminalBackend for TestableBackend<W> {
385    fn enable_raw_mode(&mut self) -> Result<(), TuiError> {
386        self.raw_mode = true;
387        Ok(())
388    }
389
390    fn disable_raw_mode(&mut self) -> Result<(), TuiError> {
391        self.raw_mode = false;
392        Ok(())
393    }
394
395    fn enter_alternate_screen(&mut self) -> Result<(), TuiError> {
396        self.alternate_screen = true;
397        // Write the actual escape sequence for testing
398        execute!(self.writer, EnterAlternateScreen)?;
399        Ok(())
400    }
401
402    fn leave_alternate_screen(&mut self) -> Result<(), TuiError> {
403        self.alternate_screen = false;
404        let _ = execute!(self.writer, LeaveAlternateScreen);
405        Ok(())
406    }
407
408    fn hide_cursor(&mut self) -> Result<(), TuiError> {
409        self.cursor_hidden = true;
410        execute!(self.writer, cursor::Hide)?;
411        Ok(())
412    }
413
414    fn show_cursor(&mut self) -> Result<(), TuiError> {
415        self.cursor_hidden = false;
416        let _ = execute!(self.writer, cursor::Show);
417        Ok(())
418    }
419
420    fn size(&self) -> Result<(u16, u16), TuiError> {
421        Ok(self.size)
422    }
423
424    fn poll(&self, _timeout: Duration) -> Result<bool, TuiError> {
425        Ok(self.poll_results.borrow_mut().pop_front().unwrap_or(false))
426    }
427
428    fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
429        self.events
430            .borrow_mut()
431            .pop_front()
432            .ok_or_else(|| TuiError::Io(io::Error::new(io::ErrorKind::WouldBlock, "no events")))
433    }
434
435    fn write_flush(
436        &mut self,
437        buffer: &mut CellBuffer,
438        renderer: &mut DiffRenderer,
439    ) -> Result<(), TuiError> {
440        renderer.flush(buffer, &mut self.writer)?;
441        self.writer.flush()?;
442        Ok(())
443    }
444
445    fn enable_mouse_capture(&mut self) -> Result<(), TuiError> {
446        self.mouse_captured = true;
447        execute!(self.writer, crossterm::event::EnableMouseCapture)?;
448        Ok(())
449    }
450
451    fn disable_mouse_capture(&mut self) -> Result<(), TuiError> {
452        self.mouse_captured = false;
453        let _ = execute!(self.writer, crossterm::event::DisableMouseCapture);
454        Ok(())
455    }
456}
457
458/// Generic terminal implementation using a backend.
459pub struct GenericTerminal<B: TerminalBackend> {
460    backend: B,
461}
462
463impl<B: TerminalBackend> GenericTerminal<B> {
464    pub fn new(backend: B) -> Self {
465        Self { backend }
466    }
467}
468
469impl<B: TerminalBackend> Terminal for GenericTerminal<B> {
470    fn enter(&mut self) -> Result<(), TuiError> {
471        self.backend.enable_raw_mode()?;
472        self.backend.enter_alternate_screen()?;
473        self.backend.hide_cursor()?;
474        Ok(())
475    }
476
477    fn leave(&mut self) -> Result<(), TuiError> {
478        self.backend.show_cursor()?;
479        self.backend.leave_alternate_screen()?;
480        self.backend.disable_raw_mode()?;
481        Ok(())
482    }
483
484    fn size(&self) -> Result<(u16, u16), TuiError> {
485        self.backend.size()
486    }
487
488    fn poll(&self, timeout: Duration) -> Result<bool, TuiError> {
489        self.backend.poll(timeout)
490    }
491
492    fn read_event(&self) -> Result<CrosstermEvent, TuiError> {
493        self.backend.read_event()
494    }
495
496    fn flush(
497        &mut self,
498        buffer: &mut CellBuffer,
499        renderer: &mut DiffRenderer,
500    ) -> Result<(), TuiError> {
501        self.backend.write_flush(buffer, renderer)
502    }
503
504    fn enable_mouse(&mut self) -> Result<(), TuiError> {
505        self.backend.enable_mouse_capture()
506    }
507
508    fn disable_mouse(&mut self) -> Result<(), TuiError> {
509        self.backend.disable_mouse_capture()
510    }
511}
512
513/// Convenience alias for crossterm-backed terminal.
514pub type CrosstermTerminal = GenericTerminal<CrosstermBackend>;
515
516/// Configuration for the TUI application.
517#[derive(Debug, Clone)]
518pub struct TuiConfig {
519    /// Tick rate in milliseconds for input polling.
520    pub tick_rate_ms: u64,
521    /// Enable mouse support.
522    pub enable_mouse: bool,
523    /// Color mode (auto-detected if not specified).
524    pub color_mode: Option<ColorMode>,
525    /// Skip Brick verification (DANGEROUS - for debugging only).
526    pub skip_verification: bool,
527    /// Target frame rate (used for budget calculation).
528    pub target_fps: u32,
529}
530
531impl Default for TuiConfig {
532    fn default() -> Self {
533        Self {
534            tick_rate_ms: 250,
535            enable_mouse: false,
536            color_mode: None,
537            target_fps: 60,
538            skip_verification: false,
539        }
540    }
541}
542
543impl TuiConfig {
544    /// Create a high-performance config (60fps, fast tick).
545    #[must_use]
546    pub fn high_performance() -> Self {
547        Self {
548            tick_rate_ms: 16,
549            target_fps: 60,
550            ..Default::default()
551        }
552    }
553
554    /// Create a power-saving config (30fps, slow tick).
555    #[must_use]
556    pub fn power_saving() -> Self {
557        Self {
558            tick_rate_ms: 100,
559            target_fps: 30,
560            ..Default::default()
561        }
562    }
563}
564
565/// Frame timing metrics.
566#[derive(Debug, Clone, Default)]
567pub struct FrameMetrics {
568    /// Time spent in verification phase.
569    pub verify_time: Duration,
570    /// Time spent in measure phase.
571    pub measure_time: Duration,
572    /// Time spent in layout phase.
573    pub layout_time: Duration,
574    /// Time spent in paint phase.
575    pub paint_time: Duration,
576    /// Total frame time.
577    pub total_time: Duration,
578    /// Frame number.
579    pub frame_count: u64,
580}
581
582/// Main TUI application runner.
583pub struct TuiApp<W: Widget> {
584    root: W,
585    config: TuiConfig,
586    input_handler: InputHandler,
587    metrics: FrameMetrics,
588    should_quit: bool,
589    color_mode: ColorMode,
590}
591
592impl<W: Widget> std::fmt::Debug for TuiApp<W> {
593    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
594        f.debug_struct("TuiApp")
595            .field("config", &self.config)
596            .field("input_handler", &self.input_handler)
597            .field("metrics", &self.metrics)
598            .field("should_quit", &self.should_quit)
599            .field("color_mode", &self.color_mode)
600            .finish_non_exhaustive()
601    }
602}
603
604/// Internal app runner that accepts a Terminal implementation.
605struct AppRunner<'a, W: Widget, T: Terminal> {
606    app: &'a mut TuiApp<W>,
607    terminal: T,
608    buffer: CellBuffer,
609    renderer: DiffRenderer,
610}
611
612impl<W: Widget, T: Terminal> AppRunner<'_, W, T> {
613    fn run_loop(&mut self) -> Result<(), TuiError> {
614        let tick_duration = Duration::from_millis(self.app.config.tick_rate_ms);
615
616        loop {
617            let frame_start = Instant::now();
618
619            // Check for terminal resize
620            let (width, height) = self.terminal.size()?;
621            if width != self.buffer.width() || height != self.buffer.height() {
622                self.buffer.resize(width, height);
623                self.renderer.reset();
624            }
625
626            // Phase 1: Verify (Jidoka gate)
627            let verify_start = Instant::now();
628            if !self.app.config.skip_verification {
629                let verification = self.app.root.verify();
630                if !verification.is_valid() {
631                    return Err(TuiError::VerificationFailed(VerificationError::from(
632                        verification,
633                    )));
634                }
635            }
636            self.app.metrics.verify_time = verify_start.elapsed();
637
638            // Phase 2: Render frame
639            self.app.render_frame(&mut self.buffer);
640
641            // Phase 3: Flush to terminal
642            self.terminal.flush(&mut self.buffer, &mut self.renderer)?;
643
644            self.app.metrics.total_time = frame_start.elapsed();
645            self.app.metrics.frame_count += 1;
646
647            // Phase 4: Handle input
648            if self.terminal.poll(tick_duration)? {
649                if let CrosstermEvent::Key(key) = self.terminal.read_event()? {
650                    if key.code == KeyCode::Char('q')
651                        || key.code == KeyCode::Char('c')
652                            && key
653                                .modifiers
654                                .contains(crossterm::event::KeyModifiers::CONTROL)
655                    {
656                        self.app.should_quit = true;
657                    }
658
659                    if let Some(event) = self.app.input_handler.convert(CrosstermEvent::Key(key)) {
660                        let _ = self.app.root.event(&event);
661                    }
662                }
663            }
664
665            if self.app.should_quit {
666                break;
667            }
668        }
669
670        Ok(())
671    }
672}
673
674impl<W: Widget> TuiApp<W> {
675    /// Create a new TUI application with the given root widget.
676    pub fn new(root: W) -> Result<Self, TuiError> {
677        // Jidoka: reject Bricks with no assertions
678        if root.assertions().is_empty() {
679            return Err(TuiError::InvalidBrick(
680                "Root widget has no assertions - every Brick must have at least one falsifiable assertion".to_string(),
681            ));
682        }
683
684        Ok(Self {
685            root,
686            config: TuiConfig::default(),
687            input_handler: InputHandler::new(),
688            metrics: FrameMetrics::default(),
689            should_quit: false,
690            color_mode: ColorMode::detect(),
691        })
692    }
693
694    /// Set the configuration.
695    #[must_use]
696    pub fn with_config(mut self, config: TuiConfig) -> Self {
697        if let Some(mode) = config.color_mode {
698            self.color_mode = mode;
699        }
700        self.config = config;
701        self
702    }
703
704    /// Set the input handler.
705    #[must_use]
706    pub fn with_input_handler(mut self, handler: InputHandler) -> Self {
707        self.input_handler = handler;
708        self
709    }
710
711    /// Get a reference to the root widget.
712    #[must_use]
713    pub fn root(&self) -> &W {
714        &self.root
715    }
716
717    /// Get a mutable reference to the root widget.
718    pub fn root_mut(&mut self) -> &mut W {
719        &mut self.root
720    }
721
722    /// Get the current frame metrics.
723    #[must_use]
724    pub fn metrics(&self) -> &FrameMetrics {
725        &self.metrics
726    }
727
728    /// Request the application to quit.
729    pub fn quit(&mut self) {
730        self.should_quit = true;
731    }
732
733    /// Run the application (blocking).
734    pub fn run(&mut self) -> Result<(), TuiError> {
735        let backend = CrosstermBackend::new();
736        let terminal = GenericTerminal::new(backend);
737        self.run_with_terminal(terminal)
738    }
739
740    /// Run the application with a custom terminal implementation.
741    /// This is the testable entry point.
742    pub fn run_with_terminal<T: Terminal>(&mut self, mut terminal: T) -> Result<(), TuiError> {
743        terminal.enter()?;
744
745        if self.config.enable_mouse {
746            terminal.enable_mouse()?;
747        }
748
749        // Get initial terminal size
750        let (width, height) = terminal.size()?;
751        let buffer = CellBuffer::new(width, height);
752        let renderer = DiffRenderer::with_color_mode(self.color_mode);
753
754        let mut runner = AppRunner {
755            app: self,
756            terminal,
757            buffer,
758            renderer,
759        };
760
761        let result = runner.run_loop();
762
763        if runner.app.config.enable_mouse {
764            runner.terminal.disable_mouse()?;
765        }
766        runner.terminal.leave()?;
767
768        result
769    }
770
771    fn render_frame(&mut self, buffer: &mut CellBuffer) {
772        let width = buffer.width();
773        let height = buffer.height();
774
775        // Phase 2a: Measure
776        let measure_start = Instant::now();
777        let constraints = Constraints::new(0.0, f32::from(width), 0.0, f32::from(height));
778        let _size = self.root.measure(constraints);
779        self.metrics.measure_time = measure_start.elapsed();
780
781        // Phase 2b: Layout
782        let layout_start = Instant::now();
783        let bounds = Rect::new(0.0, 0.0, f32::from(width), f32::from(height));
784        let _ = self.root.layout(bounds);
785        self.metrics.layout_time = layout_start.elapsed();
786
787        // Phase 2c: Paint
788        let paint_start = Instant::now();
789        {
790            let mut canvas = DirectTerminalCanvas::new(buffer);
791            self.root.paint(&mut canvas);
792        }
793        self.metrics.paint_time = paint_start.elapsed();
794    }
795}
796
797#[cfg(test)]
798#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
799#[path = "app_tests.rs"]
800mod tests;