use super::phase::{phase_to_bar_slot, ReindexPhase};
use crate::commands::format::{fmt_elapsed, fmt_secs, format_with_commas};
use colored::Colorize;
use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget, ProgressStyle};
use std::time::Duration;
pub(crate) const STAGE_LABELS: [&str; 4] = ["Scan", "Chunk", "Lexical(BM25)", "KG"];
pub(crate) const EMBED_STAR_NOTE: &str =
" * BM25 + vector-upsert commit runs concurrently with parse+embed (overlapping pipeline)";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum BarState {
Pending,
Active,
Done,
}
pub(crate) fn bar_style(slot: usize, state: BarState, elapsed_ms: Option<u64>) -> ProgressStyle {
let label = STAGE_LABELS[slot];
match state {
BarState::Pending => {
let tpl = format!(" {{spinner:.white}} {label:<5} [{{bar:40.white/white}}] pending");
ProgressStyle::with_template(&tpl)
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("\u{2588}\u{2591} ")
}
BarState::Active => {
let tpl = format!(
" {{spinner:.cyan}} {label:<5} [{{bar:40.cyan/blue}}] {{pos}}/{{len}} {{msg}}"
);
ProgressStyle::with_template(&tpl)
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("\u{2588}\u{2591} ")
}
BarState::Done => {
let t = elapsed_ms.unwrap_or(0);
let elapsed_str = fmt_elapsed(t);
let tpl = format!(
" \u{2713} {label:<5} [{{bar:40.green/green}}] {{pos}}/{{len}} \u{2014} done in {elapsed_str}"
);
ProgressStyle::with_template(&tpl)
.unwrap_or_else(|_| ProgressStyle::default_bar())
.progress_chars("\u{2588}\u{2591} ")
}
}
}
pub(crate) struct ReindexUi {
#[allow(dead_code)]
multi: MultiProgress,
header: ProgressBar,
pub(crate) stage_bars: [ProgressBar; 4],
stage_elapsed_ms: [u64; 4],
stats: ProgressBar,
pub(crate) phase: ReindexPhase,
pub(crate) bar_states: [BarState; 4],
}
impl ReindexUi {
pub(crate) fn new(index_id: &str, interactive: bool) -> Self {
let multi = if interactive {
MultiProgress::with_draw_target(ProgressDrawTarget::stderr())
} else {
MultiProgress::with_draw_target(ProgressDrawTarget::hidden())
};
let header = multi.add(ProgressBar::new(1));
if let Ok(s) = ProgressStyle::with_template("{spinner:.cyan} {msg}") {
header.set_style(s);
}
header.set_message(format!(
"{} \u{2014} {}",
ReindexPhase::Connecting.label(),
index_id.bold()
));
header.enable_steady_tick(Duration::from_millis(120));
let mut stage_bars_arr: [Option<ProgressBar>; 4] = [None, None, None, None];
for (slot, item) in stage_bars_arr.iter_mut().enumerate() {
let pb = multi.add(ProgressBar::new(1));
pb.set_style(bar_style(slot, BarState::Pending, None));
pb.set_position(0);
*item = Some(pb);
}
let stage_bars = [
stage_bars_arr[0].take().expect("slot 0"),
stage_bars_arr[1].take().expect("slot 1"),
stage_bars_arr[2].take().expect("slot 2"),
stage_bars_arr[3].take().expect("slot 3"),
];
let stats = multi.add(ProgressBar::new(1));
if let Ok(s) = ProgressStyle::with_template(" {msg}") {
stats.set_style(s);
}
stats.set_message("Waiting for daemon\u{2026}".to_string());
Self {
multi,
header,
stage_bars,
stage_elapsed_ms: [0u64; 4],
stats,
phase: ReindexPhase::Connecting,
bar_states: [BarState::Pending; 4],
}
}
pub(crate) fn set_phase(&mut self, phase: ReindexPhase, index_id: &str) {
self.phase = phase;
self.header
.set_message(format!("{} \u{2014} {}", phase.label(), index_id.bold()));
if let Some(slot) = phase_to_bar_slot(phase) {
if self.bar_states[slot] != BarState::Done {
self.bar_states[slot] = BarState::Active;
self.stage_bars[slot].set_style(bar_style(slot, BarState::Active, None));
self.stage_bars[slot].set_position(0);
}
}
}
pub(crate) fn set_total(&self, total: u64) {
if let Some(slot) = phase_to_bar_slot(self.phase) {
self.stage_bars[slot].set_length(total.max(1));
}
}
pub(crate) fn set_embed_total(&self, total: u64) {
self.stage_bars[2].set_length(total.max(1));
}
pub(crate) fn activate_embed_bar(&mut self) {
if self.bar_states[2] == BarState::Pending {
self.bar_states[2] = BarState::Active;
self.stage_bars[2].set_style(bar_style(2, BarState::Active, None));
self.stage_bars[2].set_position(0);
}
}
pub(crate) fn advance_embed_bar(&self, pos: u64) {
if self.bar_states[2] != BarState::Pending {
self.stage_bars[2].set_position(pos);
}
}
pub(crate) fn active_phase_is_embed(&self) -> bool {
phase_to_bar_slot(self.phase) == Some(2)
}
pub(crate) fn set_position(&self, pos: u64) {
if let Some(slot) = phase_to_bar_slot(self.phase) {
self.stage_bars[slot].set_position(pos);
}
}
pub(crate) fn mark_stage_done(&mut self, slot: usize, elapsed_ms: u64) {
if slot >= 4 {
return;
}
self.bar_states[slot] = BarState::Done;
self.stage_elapsed_ms[slot] = elapsed_ms;
let len = self.stage_bars[slot].length().unwrap_or(1);
self.stage_bars[slot].set_length(len.max(1));
self.stage_bars[slot].set_position(len);
self.stage_bars[slot].set_style(bar_style(slot, BarState::Done, Some(elapsed_ms)));
}
pub(crate) fn update_stats(
&self,
indexed: u64,
total_chunks: u64,
skipped: u64,
chunks_per_sec: u64,
elapsed_secs: u64,
) {
let total = if let Some(slot) = phase_to_bar_slot(self.phase) {
self.stage_bars[slot].length().unwrap_or(0)
} else {
0
};
let files_per_sec = indexed.checked_div(elapsed_secs).unwrap_or(0);
let eta = if files_per_sec > 0 && total > indexed {
fmt_secs((total - indexed) / files_per_sec)
} else {
"?".to_string()
};
let phase_label = self.phase.label();
self.stats.set_message(format!(
"{phase_label} {chunks} chunks \u{2014} {cps} cps \u{2014} Files {indexed}/{total} Skipped {skipped} Elapsed {elapsed} ETA {eta}",
chunks = format_with_commas(total_chunks),
cps = chunks_per_sec,
indexed = format_with_commas(indexed),
total = format_with_commas(total),
skipped = format_with_commas(skipped),
elapsed = fmt_secs(elapsed_secs),
eta = eta,
));
}
pub(crate) fn clear_stats(&self) {
self.stats.set_message(String::new());
}
pub(crate) fn finish(self, final_msg: String) {
for slot in 0..4 {
if self.bar_states[slot] != BarState::Done {
self.stage_bars[slot].finish_and_clear();
}
}
self.stats.finish_and_clear();
self.header.finish_with_message(final_msg);
}
pub(crate) fn abandon(self, final_msg: String) {
for bar in &self.stage_bars {
bar.abandon();
}
self.stats.abandon();
self.header.abandon_with_message(final_msg);
}
pub(crate) fn stats_bar(&self) -> ProgressBar {
self.stats.clone()
}
#[allow(dead_code)]
pub(crate) fn embed_bar(&self) -> ProgressBar {
self.stage_bars[2].clone()
}
}