mod app;
use app::{App, View, draw};
use crossterm::event::{Event, KeyCode, KeyEventKind};
use crossterm::terminal::{
BeginSynchronizedUpdate, EndSynchronizedUpdate, EnterAlternateScreen, LeaveAlternateScreen,
disable_raw_mode, enable_raw_mode,
};
use crossterm::{execute, queue};
use launchbound_report::{RunDir, build_report};
use std::io::Write;
use std::path::PathBuf;
const USAGE: &str = "\
usage: launchbound-tui <run-dir>
<run-dir> a directory written by `launchbound stage` or `launchbound tune`,
holding verdicts.json (and results.json once measured)
1 overview \u{b7} 2 ranking \u{b7} 3 rejections \u{b7} 4 progress \u{b7} j/k scroll \u{b7} q quit
";
fn main() -> anyhow::Result<()> {
let run_dir = match std::env::args().nth(1).as_deref() {
Some("-h" | "--help") => {
print!("{USAGE}");
return Ok(());
}
Some("-V" | "--version") => {
println!("launchbound-tui {}", env!("CARGO_PKG_VERSION"));
return Ok(());
}
Some(flag) if flag.starts_with('-') => {
return Err(anyhow::anyhow!("unknown option `{flag}`\n\n{USAGE}"));
}
Some(path) => PathBuf::from(path),
None => return Err(anyhow::anyhow!("{USAGE}")),
};
let run = RunDir::load(&run_dir)?;
let planned = run.plan.as_ref().map(|p| p.candidates.len()).unwrap_or(0);
let report = build_report(&run)?;
let mut app = App::new(report, planned);
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = ratatui::backend::CrosstermBackend::new(std::io::stdout());
let mut terminal = ratatui::Terminal::new(backend)?;
let result = event_loop(&mut terminal, &mut app);
disable_raw_mode()?;
execute!(std::io::stdout(), LeaveAlternateScreen)?;
result
}
fn event_loop(
terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>,
app: &mut App,
) -> anyhow::Result<()> {
loop {
queue!(std::io::stdout(), BeginSynchronizedUpdate)?;
terminal.draw(|frame| draw(frame, app))?;
queue!(std::io::stdout(), EndSynchronizedUpdate)?;
std::io::stdout().flush()?;
match crossterm::event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
KeyCode::Char('q') | KeyCode::Esc => return Ok(()),
KeyCode::Char('1') => app.select(View::Overview),
KeyCode::Char('2') => app.select(View::Ranking),
KeyCode::Char('3') => app.select(View::Rejections),
KeyCode::Char('4') => app.select(View::Progress),
KeyCode::Char('j') | KeyCode::Down => app.scroll_down(),
KeyCode::Char('k') | KeyCode::Up => app.scroll_up(),
KeyCode::Tab => {
let next = match app.view {
View::Overview => View::Ranking,
View::Ranking => View::Rejections,
View::Rejections => View::Progress,
View::Progress => View::Overview,
};
app.select(next);
}
_ => {}
},
Event::Resize(_, _) => {}
_ => {}
}
}
}