use crate::config::Config;
use crate::tui::app::App;
use crate::tui::events::Event;
use crate::tui::ui::Ui;
use anyhow::{Context, Result};
use crossterm::event::Event as CrosstermEvent;
use crossterm::execute;
use crossterm::terminal::{
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use std::io;
use std::time::Duration;
pub async fn run_tui(config: Config, refresh_interval: u64) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(config, refresh_interval)
.await
.context("Failed to initialize TUI app")?;
terminal.draw(|frame| Ui::draw(frame, &mut app))?;
app.refresh().await?;
terminal.draw(|frame| Ui::draw(frame, &mut app))?;
let cache = app.cache.clone();
let preload_config = app.config.clone();
let _preload_task = tokio::spawn(async move {
let mut temp_app = App::new(preload_config, 0).await.ok()?;
temp_app.cache = cache;
let _ = temp_app.preload_all_tabs().await;
Some(())
});
loop {
let refresh_duration = app.next_refresh_duration();
let timeout = if refresh_duration.is_zero() {
Duration::from_millis(100)
} else {
refresh_duration.min(Duration::from_millis(500))
};
if crossterm::event::poll(timeout).unwrap_or(false) {
if let Ok(CrosstermEvent::Key(key)) = crossterm::event::read() {
if !handle_event(&mut app, Event::Key(key)).await? {
break;
}
terminal.draw(|frame| Ui::draw(frame, &mut app))?;
}
} else {
if app.next_refresh_duration().is_zero() {
app.refresh().await?;
terminal.draw(|frame| Ui::draw(frame, &mut app))?;
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}
async fn handle_event(app: &mut App, event: Event) -> Result<bool> {
match event {
Event::Key(key) => handle_key_event(app, key).await,
Event::Tick => {
Ok(true)
}
Event::Error => {
app.error = Some("An error occurred".to_string());
Ok(true)
}
Event::Quit => Ok(false),
}
}
async fn handle_key_event(app: &mut App, key: crossterm::event::KeyEvent) -> Result<bool> {
use crossterm::event::{KeyCode, KeyModifiers};
if (app.info.is_some() || app.error.is_some()) && !app.show_action_menu && !app.show_help {
app.info = None;
app.error = None;
}
match key.code {
KeyCode::Char('q') | KeyCode::Char('Q') => {
return Ok(false);
}
KeyCode::Esc => {
if app.show_action_menu {
app.show_action_menu = false;
} else if !app.filter.is_empty() {
app.filter.clear();
app.update_filtered_indices();
} else if app.show_help {
app.show_help = false;
} else if app.info.is_some() || app.error.is_some() {
app.info = None;
app.error = None;
} else {
return Ok(false);
}
}
KeyCode::Down | KeyCode::Char('j') => {
if app.show_action_menu {
let actions = crate::tui::app::PrAction::all();
if app.selected_action < actions.len() - 1 {
app.selected_action += 1;
}
} else {
app.next_pr();
}
}
KeyCode::Up | KeyCode::Char('k') => {
if app.show_action_menu {
if app.selected_action > 0 {
app.selected_action -= 1;
}
} else {
app.prev_pr();
}
}
KeyCode::PageDown => {
for _ in 0..10 {
app.next_pr();
}
}
KeyCode::PageUp => {
for _ in 0..10 {
app.prev_pr();
}
}
KeyCode::Home => {
app.selected_pr = 0;
}
KeyCode::End => {
app.selected_pr = app.reviews.len().saturating_sub(1);
}
KeyCode::Tab => {
if key.modifiers.contains(KeyModifiers::SHIFT) {
app.prev_tab();
} else {
app.next_tab();
}
app.loading = true;
app.refresh().await?;
app.loading = false;
}
KeyCode::BackTab => {
app.prev_tab();
app.loading = true;
app.refresh().await?;
app.loading = false;
}
KeyCode::Char('/') => {
app.filter = String::from("/");
app.update_filtered_indices();
}
KeyCode::Char('f') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.filter.clear();
app.update_filtered_indices();
}
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::CONTROL) => {
app.force_refresh().await?;
}
KeyCode::Char('r') | KeyCode::Char('R') => {
app.refresh().await?;
}
KeyCode::Char('?') => {
app.show_help = !app.show_help;
}
KeyCode::Char(c) => {
if !app.filter.is_empty() {
if app.filter == "/" {
app.filter.clear();
}
app.filter.push(c);
app.update_filtered_indices();
}
}
KeyCode::Backspace => {
if !app.filter.is_empty() {
app.filter.pop();
if app.filter.is_empty() {
app.filter.clear();
}
app.update_filtered_indices();
}
}
KeyCode::Delete => {
app.filter.clear();
app.update_filtered_indices();
}
KeyCode::Enter => {
if app.show_action_menu {
handle_action(app).await?;
} else if app.selected_pr_item().is_some() {
app.show_action_menu = true;
app.selected_action = 0;
}
}
_ => {}
}
Ok(true)
}
async fn handle_action(app: &mut App) -> Result<()> {
use crate::tui::app::PrAction;
let actions = PrAction::all();
let selected = actions
.get(app.selected_action)
.copied()
.unwrap_or(PrAction::Cancel);
app.show_action_menu = false;
match selected {
PrAction::OpenInBrowser => {
if let Some(pr) = app.selected_pr_item() {
if let Err(e) = open::that(&pr.pr_url) {
app.error = Some(format!("Failed to open browser: {}", e));
}
}
}
PrAction::ClaudeReview => {
if let Some(pr) = app.selected_pr_item() {
app.info = Some(format!(
"Claude Code review would be launched for: {}",
pr.pr_title
));
}
}
PrAction::CopyUrl => {
if let Some(pr) = app.selected_pr_item() {
app.info = Some(format!("Copied URL: {}", pr.pr_url));
}
}
PrAction::ShowDiff => {
if let Some(pr) = app.selected_pr_item() {
app.info = Some(format!(
"Showing diff for PR #{} - {}",
pr.pr_number, pr.pr_title
));
}
}
PrAction::Approve => {
if let Some(pr) = app.selected_pr_item() {
app.info = Some(format!("Approved PR #{} - {}", pr.pr_number, pr.pr_title));
}
}
PrAction::RequestChanges => {
if let Some(pr) = app.selected_pr_item() {
app.info = Some(format!(
"Requested changes for PR #{} - {}",
pr.pr_number, pr.pr_title
));
}
}
PrAction::Cancel => {
}
}
Ok(())
}