use std::time::Duration;
use ratatui::Frame;
use ratatui::layout::Alignment;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::style::Style;
use ratatui::text::Text;
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use throbber_widgets_tui::{Throbber, ThrobberState};
use crate::components::events::{AppEvent, AppTx};
use crate::settings::themes::Theme;
pub enum IndexingProgressState {
Running {
work: tokio::task::JoinHandle<()>,
ticker: tokio::task::JoinHandle<()>,
},
Done(Duration),
Failed(String),
}
impl Drop for IndexingProgressState {
fn drop(&mut self) {
if let Self::Running { work, ticker } = self {
work.abort();
ticker.abort();
}
}
}
pub fn spawn_running(work: tokio::task::JoinHandle<()>, tx: &AppTx) -> IndexingProgressState {
let tx2 = tx.clone();
let ticker = tokio::spawn(async move {
loop {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
if tx2.send(AppEvent::Redraw).is_err() {
break;
}
}
});
IndexingProgressState::Running { work, ticker }
}
pub fn fixed_centered_rect(width: u16, height: u16, r: Rect) -> Rect {
let x = r.x + (r.width.saturating_sub(width)) / 2;
let y = r.y + (r.height.saturating_sub(height)) / 2;
Rect {
x,
y,
width: width.min(r.width),
height: height.min(r.height),
}
}
pub fn render_indexing_overlay(
f: &mut Frame,
state: &IndexingProgressState,
throbber_state: &mut ThrobberState,
theme: &Theme,
running_label: &str,
) {
let area = fixed_centered_rect(44, 5, f.area());
f.render_widget(Clear, area);
let block = Block::default()
.title("Indexing")
.borders(Borders::ALL)
.border_style(Style::default().fg(theme.accent.to_ratatui()))
.style(theme.base_style());
let inner = block.inner(area);
f.render_widget(block, area);
match state {
IndexingProgressState::Running { .. } => {
throbber_state.calc_next();
let content_width = (running_label.chars().count() as u16).saturating_add(2);
let vert = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(0),
Constraint::Length(1),
Constraint::Min(0),
])
.split(inner);
let horiz = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(0),
Constraint::Length(content_width),
Constraint::Min(0),
])
.split(vert[1]);
let throbber = Throbber::default().label(running_label).style(
Style::default()
.fg(theme.fg.to_ratatui())
.bg(theme.bg.to_ratatui()),
);
f.render_stateful_widget(throbber, horiz[1], throbber_state);
}
IndexingProgressState::Done(dur) => {
f.render_widget(
Paragraph::new(Text::raw(format!(
"✓ Done in {}s\n\n[ OK ]",
dur.as_secs()
)))
.alignment(Alignment::Center)
.style(theme.base_style()),
inner,
);
}
IndexingProgressState::Failed(msg) => {
f.render_widget(
Paragraph::new(Text::raw(format!("✗ {}\n\n[ OK ]", msg)))
.alignment(Alignment::Center)
.style(theme.base_style()),
inner,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
#[tokio::test]
async fn drop_aborts_running_tasks() {
let completed = Arc::new(AtomicBool::new(false));
let completed2 = completed.clone();
let work = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
completed2.store(true, Ordering::SeqCst);
});
let ticker = tokio::spawn(async {
tokio::time::sleep(std::time::Duration::from_secs(60)).await;
});
let state = IndexingProgressState::Running { work, ticker };
drop(state);
for _ in 0..10 {
tokio::task::yield_now().await;
}
assert!(
!completed.load(Ordering::SeqCst),
"work task should be aborted, not completed"
);
}
}