use std::time::{Duration, Instant};
use crossterm::event::{Event, EventStream, KeyEventKind, MouseButton, MouseEventKind};
use futures::StreamExt;
use ratatui::DefaultTerminal;
use tokio::sync::{mpsc, oneshot};
use crate::app::{App, Screen};
use crate::collect::{self, LayerResult};
use crate::scan::ScanOutcome;
use crate::ui::reader::Handoff;
use crate::{editor, pager, scan, tui, ui, watch};
const PREVIEW_DEBOUNCE: Duration = Duration::from_millis(80);
pub async fn run(terminal: &mut DefaultTerminal, app: &mut App) -> color_eyre::Result<()> {
let started = Instant::now();
let mut scan_rx = scan::spawn(app.root.clone(), app.root_label.clone());
let (lens_tx, mut lens_rx) = mpsc::unbounded_channel::<LayerResult>();
let mut fs_watch = watch::spawn(&app.root);
let mut rescan_rx: Option<oneshot::Receiver<ScanOutcome>> = None;
let mut rescan_again = false;
let mut events = EventStream::new();
let mut ticker = tokio::time::interval(Duration::from_millis(120));
let mut preview_target = app.preview_target_id();
let mut preview_deadline: Option<tokio::time::Instant> = None;
terminal.draw(|frame| ui::render(frame, app))?;
while !app.should_quit {
let mut redraw = false;
tokio::select! {
maybe_event = events.next() => match maybe_event {
Some(Ok(event)) => redraw = apply_event(terminal, app, event)?,
Some(Err(err)) => return Err(err.into()),
None => app.should_quit = true,
},
result = &mut scan_rx, if matches!(app.screen, Screen::Loading) => {
match result {
Ok(outcome) => app.on_scan(outcome),
Err(_) => app.on_scan_failed("scan task failed unexpectedly"),
}
redraw = true;
},
Some(result) = lens_rx.recv() => {
app.on_layer(result);
redraw = true;
},
Some(()) = async { fs_watch.as_mut().unwrap().rx.recv().await }, if fs_watch.is_some() => {
if rescan_rx.is_none() {
rescan_rx = Some(scan::spawn(app.root.clone(), app.root_label.clone()));
} else {
rescan_again = true;
}
},
outcome = async { rescan_rx.as_mut().unwrap().await }, if rescan_rx.is_some() => {
rescan_rx = None;
if let Ok(outcome) = outcome {
app.on_rescan(outcome);
}
if rescan_again {
rescan_again = false;
rescan_rx = Some(scan::spawn(app.root.clone(), app.root_label.clone()));
}
redraw = true;
},
_ = ticker.tick(), if app.is_busy() => {
app.spinner = app.spinner.wrapping_add(1);
app.elapsed = started.elapsed();
redraw = true;
},
_ = async { tokio::time::sleep_until(preview_deadline.unwrap()).await },
if preview_deadline.is_some() =>
{
preview_deadline = None;
app.refresh_preview();
redraw = true;
},
}
let target = app.preview_target_id();
if target != preview_target {
preview_target = target;
preview_deadline = target.map(|_| tokio::time::Instant::now() + PREVIEW_DEBOUNCE);
}
if let Some(lens) = app.pending_compute.take() {
let tx = lens_tx.clone();
let root = app.root.clone();
tokio::task::spawn_blocking(move || {
let _ = tx.send(collect::compute(lens, &root));
});
}
if app.should_quit {
break;
}
if redraw {
terminal.draw(|frame| ui::render(frame, app))?;
}
}
Ok(())
}
fn apply_event(
terminal: &mut DefaultTerminal,
app: &mut App,
event: Event,
) -> color_eyre::Result<bool> {
let redraw = match event {
Event::Key(key) if matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) => {
app.handle_key(key);
drain_pending_edit(terminal, app)?;
drain_reader_handoff(terminal, app)?;
drain_pending_capture(app);
true
}
Event::Mouse(m) => match m.kind {
MouseEventKind::ScrollDown => {
app.handle_scroll(m.column, m.row, 1);
true
}
MouseEventKind::ScrollUp => {
app.handle_scroll(m.column, m.row, -1);
true
}
MouseEventKind::Down(MouseButton::Left) => {
app.handle_click(m.column, m.row);
true
}
_ => false,
},
Event::Resize(_, _) => true,
_ => false,
};
Ok(redraw)
}
fn drain_pending_capture(app: &mut App) {
if let Some(on) = app.pending_capture.take() {
let _ = tui::set_mouse_capture(on);
}
}
fn drain_reader_handoff(terminal: &mut DefaultTerminal, app: &mut App) -> color_eyre::Result<()> {
if let Some(handoff) = app.take_reader_handoff() {
match handoff {
Handoff::EditAtLine { path, line } => {
tui::suspended(terminal, || editor::open_at_line(&path, line))?
}
Handoff::Pager { path } => tui::suspended(terminal, || pager::open(&path))?,
}
}
Ok(())
}
fn drain_pending_edit(terminal: &mut DefaultTerminal, app: &mut App) -> color_eyre::Result<()> {
if let Some(path) = app.pending_edit.take() {
tui::suspended(terminal, || editor::open(&path))?;
}
Ok(())
}