1pub mod app;
13pub mod theme;
14pub mod ui;
15
16use crate::error::Result;
17use crossterm::event::{self, Event, KeyEventKind};
18use crossterm::terminal::{
19 disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
20};
21use crossterm::ExecutableCommand;
22use ratatui::backend::CrosstermBackend;
23use ratatui::Terminal;
24use std::io::stdout;
25use std::time::Duration;
26
27fn err(e: impl std::fmt::Display) -> crate::error::VirtuosoError {
28 crate::error::VirtuosoError::Execution(e.to_string())
29}
30
31pub fn run_tui() -> Result<()> {
32 let mut app = app::state::App::new();
33 let theme = theme::Theme::detect();
34
35 enable_raw_mode().map_err(err)?;
36 stdout().execute(EnterAlternateScreen).map_err(err)?;
37 let backend = CrosstermBackend::new(stdout());
38 let mut terminal = Terminal::new(backend).map_err(err)?;
39
40 let result = run_loop(&mut terminal, &mut app, &theme);
41
42 let _ = disable_raw_mode();
43 let _ = stdout().execute(LeaveAlternateScreen);
44 result
45}
46
47fn run_loop(
48 terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
49 app: &mut app::state::App,
50 theme: &theme::Theme,
51) -> Result<()> {
52 loop {
53 terminal
54 .draw(|frame| ui::draw(frame, app, theme))
55 .map_err(err)?;
56
57 if !event::poll(Duration::from_millis(500)).map_err(err)? {
58 app.spinner_frame = app.spinner_frame.wrapping_add(1);
59 app.clear_expired_status();
60 continue;
61 }
62
63 if let Event::Key(key) = event::read().map_err(err)? {
64 if key.kind != KeyEventKind::Press {
65 continue;
66 }
67 let action = app::on_key(app, key);
68 app::handle_action(app, action);
69 if app.should_quit {
70 break;
71 }
72 }
73 }
74 Ok(())
75}