use crate::tui::{AppState, controls::handle_key_event, render::render, state::AppMode};
use crossterm::{
event::{self, Event},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, prelude::CrosstermBackend};
use std::{io::stdout, panic::AssertUnwindSafe, time::Duration};
pub async fn run_app() -> color_eyre::Result<()> {
let mut state = AppState::new();
setup_terminal()?;
let result = app(&mut state).await;
restore_terminal()?;
result
}
async fn app(state: &mut AppState) -> color_eyre::Result<()> {
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
loop {
if let Err(panic_info) = std::panic::catch_unwind(AssertUnwindSafe(|| {
terminal.draw(|f| render(f, state)).ok();
})) {
let msg = if let Some(s) = panic_info.downcast_ref::<String>() {
format!("Render panic: {s}")
} else if let Some(s) = panic_info.downcast_ref::<&str>() {
format!("Render panic: {s}")
} else {
"Render panic: unknown".to_string()
};
state.cmdline.set_error(msg);
let _ = terminal.draw(|f| {
let area = f.area();
let line = ratatui::text::Line::from(ratatui::text::Span::styled(
" Render error — check cmdline ",
ratatui::style::Style::default().fg(ratatui::style::Color::Red),
));
f.render_widget(ratatui::widgets::Paragraph::new(vec![line]), area);
});
}
if state.pending_connection.is_some() {
handle_pending_connection(state, &mut terminal).await?;
continue;
}
let mut handled_key = false;
tokio::select! {
_ = tokio::signal::ctrl_c() => {
state.should_quit = true;
}
ready = async { event::poll(Duration::from_millis(50)) } => {
if ready? {
if let Event::Key(key) = event::read()? {
handle_key_event(key, state).await?;
handled_key = true;
}
}
}
}
if state.should_quit {
break;
}
if !handled_key {
crate::tui::controls::ops::maybe_schedule_live_table_refresh(state);
crate::tui::controls::ops::run_pending_tasks(state).await?;
}
}
Ok(())
}
fn setup_terminal() -> color_eyre::Result<()> {
enable_raw_mode()?;
execute!(stdout(), EnterAlternateScreen)?;
Ok(())
}
fn restore_terminal() -> color_eyre::Result<()> {
execute!(stdout(), LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}
async fn handle_pending_connection(
state: &mut AppState,
terminal: &mut Terminal<CrosstermBackend<std::io::Stdout>>,
) -> color_eyre::Result<()> {
let Some(db) = state.pending_connection.take() else {
return Ok(());
};
let mut interval = tokio::time::interval(Duration::from_millis(100));
let mut spinner_frame = 0usize;
const SPINNER: [char; 10] = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let conn = db.connection.clone();
let connect_fut = async move {
let pool = crate::connection::connect_db(conn).await?;
let tables = crate::connection::list_tables(&pool).await?;
Ok::<_, color_eyre::eyre::Error>((pool, tables))
};
tokio::pin!(connect_fut);
loop {
tokio::select! {
biased;
_ = tokio::signal::ctrl_c() => {
state.should_quit = true;
break;
}
result = &mut connect_fut => {
match result {
Ok((pool, tables)) => {
state.connection = Some(db);
state.pool = Some(pool);
state.tables = tables;
state.table_cache = std::collections::HashMap::new();
state.tabs = vec![crate::tui::state::Tab::new()];
state.active_tab = 0;
state.mode = AppMode::Dashboard;
state.cmdline.clear_loading();
}
Err(e) => {
state.cmdline.set_error(format!("Connection failed: {e}"));
}
}
break;
}
_ = interval.tick() => {
let ch = SPINNER[spinner_frame % SPINNER.len()];
state.cmdline.set_loading(format!("{ch} Connecting to {}...", db.name));
spinner_frame += 1;
let _ = std::panic::catch_unwind(AssertUnwindSafe(|| {
terminal.draw(|f| render(f, state)).ok();
}));
}
}
}
Ok(())
}