kimun_notes/components/
indexing.rs1use std::time::Duration;
2
3use ratatui::Frame;
4use ratatui::layout::Alignment;
5use ratatui::layout::{Constraint, Direction, Layout};
6use ratatui::style::Style;
7use ratatui::text::Text;
8use ratatui::widgets::Paragraph;
9use throbber_widgets_tui::{Throbber, ThrobberState};
10
11use crate::components::events::{AppEvent, AppTx};
12use crate::settings::themes::Theme;
13
14pub enum IndexingProgressState {
15 Running {
16 work: tokio::task::JoinHandle<()>,
17 ticker: tokio::task::JoinHandle<()>,
18 },
19 Done(Duration),
20 Failed(String),
21}
22
23impl Drop for IndexingProgressState {
24 fn drop(&mut self) {
25 if let Self::Running { work, ticker } = self {
26 work.abort();
27 ticker.abort();
28 }
29 }
30}
31
32pub fn spawn_running(work: tokio::task::JoinHandle<()>, tx: &AppTx) -> IndexingProgressState {
33 let tx2 = tx.clone();
34 let ticker = tokio::spawn(async move {
35 loop {
36 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
37 if tx2.send(AppEvent::Redraw).is_err() {
38 break;
39 }
40 }
41 });
42 IndexingProgressState::Running { work, ticker }
43}
44
45pub use crate::components::fixed_centered_rect;
46
47pub fn render_indexing_overlay(
53 f: &mut Frame,
54 state: &IndexingProgressState,
55 throbber_state: &mut ThrobberState,
56 theme: &Theme,
57 running_label: &str,
58) {
59 let area = fixed_centered_rect(44, 5, f.area());
60 let inner = crate::components::panel::modal_chrome(
61 f,
62 area,
63 theme,
64 crate::components::panel::ModalSpec {
65 title: Some("Indexing"),
66 border: Some(Style::default().fg(theme.accent.to_ratatui())),
67 bg: crate::components::panel::ModalBg::Base,
68 },
69 );
70
71 match state {
72 IndexingProgressState::Running { .. } => {
73 throbber_state.calc_next();
74 let content_width = (running_label.chars().count() as u16).saturating_add(2);
76 let vert = Layout::default()
77 .direction(Direction::Vertical)
78 .constraints([
79 Constraint::Min(0),
80 Constraint::Length(1),
81 Constraint::Min(0),
82 ])
83 .split(inner);
84 let horiz = Layout::default()
85 .direction(Direction::Horizontal)
86 .constraints([
87 Constraint::Min(0),
88 Constraint::Length(content_width),
89 Constraint::Min(0),
90 ])
91 .split(vert[1]);
92 let throbber = Throbber::default().label(running_label).style(
93 Style::default()
94 .fg(theme.fg.to_ratatui())
95 .bg(theme.bg.to_ratatui()),
96 );
97 f.render_stateful_widget(throbber, horiz[1], throbber_state);
98 }
99 IndexingProgressState::Done(dur) => {
100 f.render_widget(
101 Paragraph::new(Text::raw(format!(
102 "✓ Done in {}s\n\n[ OK ]",
103 dur.as_secs()
104 )))
105 .alignment(Alignment::Center)
106 .style(theme.base_style()),
107 inner,
108 );
109 }
110 IndexingProgressState::Failed(msg) => {
111 f.render_widget(
112 Paragraph::new(Text::raw(format!("✗ {}\n\n[ OK ]", msg)))
113 .alignment(Alignment::Center)
114 .style(theme.base_style()),
115 inner,
116 );
117 }
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124 use std::sync::{
125 Arc,
126 atomic::{AtomicBool, Ordering},
127 };
128
129 #[tokio::test]
130 async fn drop_aborts_running_tasks() {
131 let completed = Arc::new(AtomicBool::new(false));
132 let completed2 = completed.clone();
133
134 let work = tokio::spawn(async move {
135 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
136 completed2.store(true, Ordering::SeqCst);
137 });
138 let ticker = tokio::spawn(async {
139 tokio::time::sleep(std::time::Duration::from_secs(60)).await;
140 });
141
142 let state = IndexingProgressState::Running { work, ticker };
143 drop(state);
144
145 for _ in 0..10 {
148 tokio::task::yield_now().await;
149 }
150
151 assert!(
152 !completed.load(Ordering::SeqCst),
153 "work task should be aborted, not completed"
154 );
155 }
156}