use anyhow::Result;
use crossterm::event::{poll, read, Event as CrosstermEvent, KeyCode, KeyEventKind};
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::prelude::{Backend, CrosstermBackend, Terminal};
use std::io::{self, stdout};
use std::sync::Arc;
use std::time::Duration;
use crate::core::OpenCrates;
use crate::tui::app::App;
use crate::tui::event::{Event, EventHandler};
use crate::tui::ui as ui_module;
use tracing::{debug, error, info, warn};
pub mod app;
pub mod event;
pub mod ui;
pub struct Tui<B: Backend> {
terminal: Terminal<B>,
pub app: App,
event_handler: EventHandler,
}
pub async fn run_tui(core: Arc<OpenCrates>) -> Result<()> {
info!("Initializing OpenCrates TUI");
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout());
let terminal = Terminal::new(backend)?;
let event_handler = EventHandler::new(16);
let mut tui = Tui::new(terminal, event_handler, core);
let result = tui.run().await;
tui.exit()?;
match result {
Ok(()) => {
info!("TUI exited successfully");
Ok(())
}
Err(e) => {
error!("TUI exited with error: {}", e);
Err(e)
}
}
}
pub async fn run_tui_with_recovery(core: Arc<OpenCrates>) -> Result<()> {
let mut retry_count = 0;
const MAX_RETRIES: u32 = 3;
loop {
match run_tui(core.clone()).await {
Ok(()) => return Ok(()),
Err(e) => {
error!("TUI failed: {}", e);
retry_count += 1;
if retry_count >= MAX_RETRIES {
error!("TUI failed after {} retries", MAX_RETRIES);
return Err(e);
}
warn!(
"Retrying TUI startup (attempt {}/{})",
retry_count + 1,
MAX_RETRIES
);
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
}
impl<B: Backend + std::io::Write> Tui<B> {
pub fn new(terminal: Terminal<B>, event_handler: EventHandler, core: Arc<OpenCrates>) -> Self {
debug!("Creating new TUI instance");
let app = App {
opencrates: core,
analysis: None,
health: None,
dependencies: Vec::new(),
should_quit: false,
};
Self {
terminal,
app,
event_handler,
}
}
pub async fn run(&mut self) -> Result<()> {
debug!("Starting TUI main loop");
self.init()?;
if let Err(e) = self.app.refresh().await {
warn!("Failed to refresh initial data: {}", e);
}
let mut frame_count = 0;
let start_time = std::time::Instant::now();
loop {
match self
.terminal
.draw(|frame| ui_module::render(&mut self.app, frame))
{
Ok(_) => {
frame_count += 1;
if frame_count % 100 == 0 {
let fps = f64::from(frame_count) / start_time.elapsed().as_secs_f64();
debug!("Rendered {} frames, average FPS: {:.2}", frame_count, fps);
}
}
Err(e) => {
error!("Failed to render UI: {}", e);
return Err(e.into());
}
}
match self.event_handler.next().await {
Ok(event) => match event {
Event::Quit => break,
Event::Custom(_) => {}
Event::Key(key_event) => {
debug!("Key event: {:?}", key_event);
if key_event.code == KeyCode::Char('q') {
info!("User requested quit");
self.app.should_quit = true;
}
if key_event.code == KeyCode::Char('r') {
info!("User requested refresh");
if let Err(e) = self.app.refresh().await {
warn!("Failed to refresh data: {}", e);
}
}
if key_event.code == KeyCode::Char('?') {
debug!("Help toggled");
}
if self.app.should_quit {
break;
}
}
Event::Tick => {
if let Err(e) = self.app.update().await {
warn!("Failed to update app: {}", e);
}
}
Event::Mouse(mouse_event) => {
debug!("Mouse event: {:?}", mouse_event);
}
Event::Resize(width, height) => {
debug!("Terminal resized to {}x{}", width, height);
if let Err(e) = self
.terminal
.resize(ratatui::prelude::Rect::new(0, 0, width, height))
{
warn!("Failed to handle terminal resize: {}", e);
}
}
},
Err(e) => {
error!("Event handler error: {}", e);
return Err(e);
}
}
}
info!("TUI main loop completed");
Ok(())
}
pub fn init(&mut self) -> Result<()> {
debug!("Initializing terminal");
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
self.terminal.hide_cursor()?;
self.terminal.clear()?;
Ok(())
}
pub fn exit(&mut self) -> Result<()> {
debug!("Cleaning up terminal");
disable_raw_mode()?;
execute!(stdout(), LeaveAlternateScreen)?;
self.terminal.show_cursor()?;
Ok(())
}
pub fn size(&self) -> io::Result<(u16, u16)> {
let size = self.terminal.size()?;
Ok((size.width, size.height))
}
pub fn force_redraw(&mut self) -> Result<()> {
debug!("Forcing terminal redraw");
self.terminal.clear()?;
Ok(())
}
pub fn app(&self) -> &App {
&self.app
}
pub fn app_mut(&mut self) -> &mut App {
&mut self.app
}
pub fn should_continue(&self) -> bool {
!self.app.should_quit
}
pub async fn process_events_batch(&mut self, max_events: usize) -> Result<usize> {
let mut processed = 0;
while processed < max_events {
if !poll(Duration::from_millis(1))? {
break;
}
match self.event_handler.next().await {
Ok(event) => {
match event {
Event::Key(key_event) => {
if key_event.code == KeyCode::Char('q') {
self.app.should_quit = true;
}
}
Event::Tick => {
if let Err(e) = self.app.update().await {
warn!("Failed to update app in batch processing: {}", e);
}
}
_ => {}
}
processed += 1;
if self.app.should_quit {
break;
}
}
Err(e) => {
warn!("Error in batch event processing: {}", e);
break;
}
}
}
debug!("Processed {} events in batch", processed);
Ok(processed)
}
pub fn set_mouse_support(&mut self, enable: bool) -> Result<()> {
if enable {
debug!("Enabling mouse support");
execute!(stdout(), crossterm::event::EnableMouseCapture)?;
} else {
debug!("Disabling mouse support");
execute!(stdout(), crossterm::event::DisableMouseCapture)?;
}
Ok(())
}
pub fn set_title(&mut self, title: &str) -> Result<()> {
debug!("Setting terminal title: {}", title);
execute!(stdout(), crossterm::terminal::SetTitle(title))?;
Ok(())
}
}