use std::time::Duration;
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use super::render::render;
use super::state::{TuiState, View, WorkStatus};
use super::work::{WorkMode, WorkerHandle};
const TICK: Duration = Duration::from_millis(100);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Action {
Quit,
NextView,
GoTo(View),
Move(isize),
Toggle,
ToggleAll,
RunFull,
RunCorrelateOnly,
CycleFilter,
Reload,
ToggleHelp,
None,
}
pub fn key_to_action(code: KeyCode, modifiers: KeyModifiers) -> Action {
if modifiers.contains(KeyModifiers::CONTROL) && code == KeyCode::Char('c') {
return Action::Quit;
}
match code {
KeyCode::Char('q') | KeyCode::Esc => Action::Quit,
KeyCode::Tab => Action::NextView,
KeyCode::Char('1') => Action::GoTo(View::Repos),
KeyCode::Char('2') => Action::GoTo(View::Progress),
KeyCode::Char('3') => Action::GoTo(View::Results),
KeyCode::Up | KeyCode::Char('k') => Action::Move(-1),
KeyCode::Down | KeyCode::Char('j') => Action::Move(1),
KeyCode::PageUp => Action::Move(-10),
KeyCode::PageDown => Action::Move(10),
KeyCode::Char(' ') => Action::Toggle,
KeyCode::Char('a') => Action::ToggleAll,
KeyCode::Char('r') => Action::RunFull,
KeyCode::Char('c') => Action::RunCorrelateOnly,
KeyCode::Char('f') => Action::CycleFilter,
KeyCode::Char('g') => Action::Reload,
KeyCode::Char('?') => Action::ToggleHelp,
_ => Action::None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Effect {
Nothing,
Start(WorkMode),
Reload,
}
pub fn apply(state: &mut TuiState, action: Action) -> Effect {
if action != Action::None {
state.message = None;
}
if action != Action::Quit {
state.quit_armed = false;
}
match action {
Action::None => Effect::Nothing,
Action::Quit => {
if state.show_help {
state.show_help = false;
} else if state.status.is_running() && !state.quit_armed {
state.quit_armed = true;
state.set_message(
"a run is in flight — press [q]/[Esc] again to abandon it mid-write",
);
} else {
state.quit = true;
}
Effect::Nothing
}
Action::NextView => {
state.view = state.view.next();
Effect::Nothing
}
Action::GoTo(view) => {
state.view = view;
Effect::Nothing
}
Action::Move(delta) => {
state.move_cursor(delta);
Effect::Nothing
}
Action::Toggle => {
if state.view == View::Repos {
state.toggle_selected();
}
Effect::Nothing
}
Action::ToggleAll => {
if state.view == View::Repos {
state.toggle_select_all();
}
Effect::Nothing
}
Action::RunFull => start(state, WorkMode::PullAndCorrelate),
Action::RunCorrelateOnly => start(state, WorkMode::CorrelateOnly),
Action::CycleFilter => {
state.filter = state.filter.next();
state.row_cursor = 0;
Effect::Reload
}
Action::Reload => Effect::Reload,
Action::ToggleHelp => {
state.show_help = !state.show_help;
Effect::Nothing
}
}
}
fn start(state: &mut TuiState, mode: WorkMode) -> Effect {
if state.status.is_running() {
state.set_message("a run is already in flight");
return Effect::Nothing;
}
if mode == WorkMode::PullAndCorrelate && state.offline {
state.set_message("--correlate-only is set — press [c] to correlate without a pull");
return Effect::Nothing;
}
if mode == WorkMode::PullAndCorrelate && state.selected_repositories().is_empty() {
state.set_message("no repositories selected — press [Space] or [a] in the Repos view");
return Effect::Nothing;
}
state.status = WorkStatus::Running;
state.view = View::Progress;
Effect::Start(mode)
}
pub fn run_loop<B: std::io::Write>(
terminal: &mut Terminal<CrosstermBackend<B>>,
state: &mut TuiState,
worker: &mut WorkerHandle,
) -> anyhow::Result<()> {
loop {
state.pump_progress();
if let Some(summary) = worker.poll_finished() {
state.status = WorkStatus::Finished(summary);
worker.reload_results(state);
}
terminal.draw(|frame| render(frame, state))?;
if !event::poll(TICK)? {
continue;
}
let Event::Key(KeyEvent {
code,
modifiers,
kind,
..
}) = event::read()?
else {
continue;
};
if kind != KeyEventKind::Press {
continue;
}
match apply(state, key_to_action(code, modifiers)) {
Effect::Nothing => {}
Effect::Start(mode) => worker.start(state, mode),
Effect::Reload => worker.reload_results(state),
}
if state.quit {
return Ok(());
}
}
}