Skip to main content

lingora_tui/
app.rs

1use std::rc::Rc;
2
3use crossterm::event;
4use lingora_core::prelude::*;
5use rat_event::{HandleEvent, Regular};
6use rat_text::HasScreenCursor;
7use ratatui::{DefaultTerminal, prelude::*};
8use ratatui_themes::ThemeName;
9
10use crate::{
11    args::TuiArgs,
12    error::TuiError,
13    pages::{AppView, AppViewState},
14    theme::LingoraTheme,
15    user_preferences::UserPreferences,
16};
17
18/// The main application state and driver for the interactive terminal user interface.
19///
20/// `App` owns:
21/// - The visual theme (`LingoraTheme`)
22/// - The `AuditResult` (shared across widgets/views)
23/// - The current application view state (`AppViewState`)
24///
25/// Responsibilities:
26/// - Initialize from configuration and audit result
27/// - Run the main event/draw loop
28/// - Delegate rendering to `AppView` (stateful widget)
29/// - Forward keyboard/mouse events to the view state
30/// - Manage cursor visibility and position
31pub struct App {
32    audit_result: Rc<AuditResult>,
33    state: AppViewState,
34}
35
36impl App {
37    /// Creates a new `App` instance from settings and a completed audit result.
38    ///
39    /// - Initializes the theme
40    /// - Wraps the audit result in `Rc` for shared access
41    /// - Creates initial view state from settings and result
42    pub fn new(settings: LingoraToml, audit_result: AuditResult) -> Self {
43        let theme = LingoraTheme::new(ThemeName::Dracula, audit_result.workspace());
44        let audit_result = Rc::new(audit_result);
45        let state = AppViewState::new(&settings, theme, audit_result.clone());
46
47        Self {
48            audit_result,
49            state,
50        }
51    }
52
53    /// Replaces the base theme and returns `self` (builder-style).
54    pub fn set_theme(mut self, theme: ThemeName) -> Self {
55        self.state.set_theme(theme);
56        self
57    }
58
59    /// Runs the main TUI event/draw loop until the user quits.
60    ///
61    /// Loop steps:
62    /// 1. Draw current frame using `AppView` widget
63    /// 2. Read next crossterm event
64    /// 3. Handle event (keyboard, mouse, resize) via `AppViewState`
65    /// 4. Repeat until `state.is_running()` returns `false` (usually on 'q' or Ctrl+C)
66    ///
67    /// # Errors
68    /// Propagates terminal I/O or event reading failures as `TuiError::Io`.
69    pub fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<(), TuiError> {
70        while self.state.is_running() {
71            terminal.draw(|frame| self.draw(frame))?;
72            self.handle_events()?;
73        }
74
75        Ok(())
76    }
77
78    fn draw(&mut self, frame: &mut Frame) {
79        let mut view = AppView::new(&self.audit_result);
80
81        frame.render_stateful_widget(&mut view, frame.area(), &mut self.state);
82        if let Some(cursor) = self.state.screen_cursor() {
83            frame.set_cursor_position(cursor);
84        }
85    }
86
87    fn handle_events(&mut self) -> Result<(), TuiError> {
88        let event = event::read()?;
89        self.state.handle(&event, Regular);
90        Ok(())
91    }
92}
93
94impl TryFrom<LingoraToml> for App {
95    type Error = TuiError;
96
97    fn try_from(settings: LingoraToml) -> Result<Self, Self::Error> {
98        let engine = AuditEngine::try_from(&settings)?;
99        let audit_result = engine.run()?;
100
101        Ok(App::new(settings, audit_result))
102    }
103}
104
105impl TryFrom<&TuiArgs> for App {
106    type Error = TuiError;
107
108    fn try_from(value: &TuiArgs) -> Result<Self, Self::Error> {
109        let settings = LingoraToml::try_from(value.core_args())?;
110        let theme = match value.theme() {
111            Some(theme) => theme,
112            None => UserPreferences::load().theme(),
113        };
114        Self::try_from(settings).map(|app| app.set_theme(theme))
115    }
116}