use std::collections::HashSet;
use std::io;
use std::io::Stdout;
use std::path::Path;
use std::thread;
use std::time::Duration;
use std::time::Instant;
use crossterm::event::Event;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use tui_pane::PERF_LOG_TARGET;
use tui_pane::SLOW_FRAME_MS;
use tui_pane::TrackedItemKey;
use super::frame_metrics::FrameMetrics;
use super::processes;
use super::run;
use super::tree_state;
use crate::channel;
use crate::channel::Receiver;
use crate::channel::Select;
use crate::channel::TryRecvError;
use crate::project::AbsolutePath;
use crate::tui::app::App;
use crate::tui::app::PollBackgroundStats;
use crate::tui::input;
use crate::tui::render;
pub(super) fn spawn_input_thread() -> Receiver<Event> {
let (event_sender, event_receiver) = channel::unbounded();
thread::spawn(move || {
while let Ok(event) = crossterm::event::read() {
if event_sender.send(event).is_err() {
break;
}
}
});
event_receiver
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum InputChannelState {
#[default]
Connected,
Disconnected,
}
impl InputChannelState {
const fn is_disconnected(self) -> bool { matches!(self, Self::Disconnected) }
}
struct InputDrain {
count: usize,
elapsed: Duration,
channel_state: InputChannelState,
}
pub(super) fn event_loop(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
app: &mut App,
input_rx: &Receiver<Event>,
) -> io::Result<()> {
let mut rearmed_after_first_draw = false;
loop {
let frame_started = Instant::now();
let input = process_input_frame(app, input_rx);
if input.channel_state.is_disconnected() {
tracing::error!("input channel disconnected; exiting event loop");
return Ok(());
}
if app.framework.quit_requested() || app.framework.restart_requested() {
return Ok(());
}
let (bg_stats, bg_elapsed) = poll_background_frame(app);
let tick_now = Instant::now();
let cpu_elapsed = measure(|| app.panes.cpu_tick());
let run_targets_elapsed = measure(|| app.running_targets_tick(tick_now));
app.scan.prune_shimmers(tick_now);
let rows_elapsed = measure(|| app.ensure_visible_rows_cached());
let disk_elapsed = measure(|| app.ensure_disk_cache());
let fit_elapsed = measure(|| app.ensure_fit_widths_cached());
let detail_elapsed = measure(|| app.ensure_detail_cached());
let draw_elapsed = draw_frame(terminal, app)?;
if !rearmed_after_first_draw {
let _ = run::rearm_input_modes();
rearmed_after_first_draw = true;
}
if app.framework.quit_requested() || app.framework.restart_requested() {
flush_pending_selection(app);
break;
}
spawn_pending_background_tasks(app);
log_slow_frame(
app,
&bg_stats,
&FrameMetrics {
frame_elapsed: frame_started.elapsed(),
input_elapsed: input.elapsed,
bg_elapsed,
cpu_elapsed,
run_targets_elapsed,
rows_elapsed,
disk_elapsed,
fit_elapsed,
detail_elapsed,
draw_elapsed,
input_count: input.count,
},
);
wait_for_event(app, input_rx);
}
Ok(())
}
fn wait_for_event(app: &App, input_rx: &Receiver<Event>) {
let timeout = app.animation_timeout();
let mut select = Select::new();
select.recv(input_rx);
select.recv(app.background.background_receiver());
select.recv(app.background.ci_fetch_rx());
select.recv(app.background.clean_rx());
select.recv(app.background.example_rx());
if app.panes.cpu.is_sampling() {
select.recv(app.panes.cpu.sample_rx());
}
let _ = select.ready_timeout(timeout);
}
fn process_input_frame(app: &mut App, input_rx: &Receiver<Event>) -> InputDrain {
let started = Instant::now();
let mut count = 0usize;
let mut channel_state = InputChannelState::Connected;
loop {
match input_rx.try_recv() {
Ok(event) => {
count += 1;
tracing::trace!(
target: PERF_LOG_TARGET,
event = %tui_pane::event_label(&event),
"input_event_received"
);
input::handle_event(app, &event);
if app.framework.quit_requested() || app.framework.restart_requested() {
break;
}
},
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => {
channel_state = InputChannelState::Disconnected;
break;
},
}
}
if count == 0 {
flush_deferred_selection(app);
}
InputDrain {
count,
elapsed: started.elapsed(),
channel_state,
}
}
fn flush_deferred_selection(app: &mut App) {
if app.project_list.sync().is_changed() {
tree_state::save_tree_state(app);
app.project_list.mark_sync_stable();
}
}
fn flush_pending_selection(app: &App) {
if app.project_list.sync().is_changed() {
tree_state::save_tree_state(app);
}
}
fn poll_background_frame(app: &mut App) -> (PollBackgroundStats, Duration) {
let started = Instant::now();
app.maybe_reload_config_from_disk();
app.maybe_reload_keymap_from_disk();
app.maybe_reload_themes_from_disk();
let stats = app.poll_background();
app.tick_startup_panel();
(stats, started.elapsed())
}
fn measure(action: impl FnOnce()) -> Duration {
let started = Instant::now();
action();
started.elapsed()
}
fn draw_frame(
terminal: &mut Terminal<CrosstermBackend<Stdout>>,
app: &mut App,
) -> io::Result<Duration> {
let started = Instant::now();
terminal.draw(|frame| render::ui(frame, app))?;
Ok(started.elapsed())
}
fn spawn_pending_background_tasks(app: &mut App) {
if let Some(run) = app.inflight.take_pending_example_run() {
processes::spawn_example_process(app, &run);
}
if let Some(pending) = app.inflight.pending_cleans_mut().pop_front() {
processes::spawn_clean_process(app, &pending);
}
if let Some(fetch) = app.inflight.take_pending_ci_fetch() {
let abs_path = AbsolutePath::from(Path::new(&fetch.project_path));
if processes::spawn_ci_fetch(app, &fetch) {
app.ci.fetch_tracker.start(abs_path);
app.scan.bump_generation();
} else if let Some(task_id) = app.ci.take_fetch_toast() {
let empty: HashSet<TrackedItemKey> = HashSet::new();
app.framework.toasts.complete_missing_items(task_id, &empty);
}
}
}
fn log_slow_frame(app: &App, bg_stats: &PollBackgroundStats, metrics: &FrameMetrics) {
if metrics.frame_elapsed.as_millis() < SLOW_FRAME_MS {
return;
}
tracing::trace!(
target: PERF_LOG_TARGET,
frame_ms = tui_pane::perf_log_ms(metrics.frame_elapsed.as_millis()),
input_ms = tui_pane::perf_log_ms(metrics.input_elapsed.as_millis()),
bg_ms = tui_pane::perf_log_ms(metrics.bg_elapsed.as_millis()),
cpu_ms = tui_pane::perf_log_ms(metrics.cpu_elapsed.as_millis()),
run_targets_ms = tui_pane::perf_log_ms(metrics.run_targets_elapsed.as_millis()),
rows_ms = tui_pane::perf_log_ms(metrics.rows_elapsed.as_millis()),
disk_ms = tui_pane::perf_log_ms(metrics.disk_elapsed.as_millis()),
fit_ms = tui_pane::perf_log_ms(metrics.fit_elapsed.as_millis()),
detail_ms = tui_pane::perf_log_ms(metrics.detail_elapsed.as_millis()),
draw_ms = tui_pane::perf_log_ms(metrics.draw_elapsed.as_millis()),
input_count = metrics.input_count,
bg_msgs = bg_stats.bg_msgs,
disk_usage_msgs = bg_stats.disk_usage_msgs,
git_info_msgs = bg_stats.git_info_msgs,
lint_status_msgs = bg_stats.lint_status_msgs,
language_progress_msgs = bg_stats.language_progress_msgs,
ci_msgs = bg_stats.ci_msgs,
example_msgs = bg_stats.example_msgs,
tree_results = bg_stats.tree_results,
fit_results = bg_stats.fit_results,
disk_results = bg_stats.disk_results,
needs_rebuild = bg_stats.rebuild_status.needs_rebuild(),
items = app.project_list.len(),
scan_complete = app.scan.is_complete(),
"slow_frame"
);
}