use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::OnceLock;
use std::time::Instant;
use tracing::debug;
use crate::file::{FileEntry, Priority, Result};
use crate::gather::dedup::dedup_decision;
use crate::gather::{
DesktopSource, MatchIndex, Origin, Source, StartMenuSource, WindowsAppsSource,
};
#[derive(Debug)]
pub struct WeightedEntry {
pub entry: Box<dyn FileEntry>,
pub origin: Origin,
pub source_priority: Priority,
index: OnceLock<MatchIndex>,
}
impl WeightedEntry {
#[must_use]
pub fn new(entry: Box<dyn FileEntry>, origin: Origin, source_priority: Priority) -> Self {
Self {
entry,
origin,
source_priority,
index: OnceLock::new(),
}
}
pub fn priority_score(&self) -> f32 {
self.source_priority.0 * self.entry.priority().0
}
pub fn score(&self, query: &str) -> Option<f32> {
let q: Cow<'_, str> = if query.chars().any(char::is_uppercase) {
Cow::Owned(query.to_lowercase())
} else {
Cow::Borrowed(query)
};
Some(self.index().score(&q)? * self.priority_score())
}
fn index(&self) -> &MatchIndex {
self.index
.get_or_init(|| MatchIndex::from_entry(&*self.entry))
}
}
impl AsRef<Self> for WeightedEntry {
fn as_ref(&self) -> &Self {
self
}
}
#[derive(Debug)]
pub struct Gatherer {
sources: Vec<(Box<dyn Source>, Priority)>,
dedup: bool,
}
impl Default for Gatherer {
fn default() -> Self {
Self::new()
}
}
impl Gatherer {
#[must_use]
pub fn new() -> Self {
Self {
sources: Vec::new(),
dedup: true,
}
}
#[must_use]
pub fn with_desktop(self, priority: Priority) -> Self {
self.with_source(DesktopSource::new(), priority)
}
#[must_use]
pub fn with_start_menu(self, priority: Priority) -> Self {
self.with_source(StartMenuSource::new(), priority)
}
#[must_use]
pub fn with_windows_apps(self, priority: Priority) -> Self {
self.with_source(WindowsAppsSource::new(), priority)
}
#[must_use]
pub fn with_source<S>(mut self, source: S, priority: Priority) -> Self
where
S: Source + 'static,
{
self.sources.push((Box::new(source), priority));
self
}
#[must_use]
pub const fn with_dedup(mut self, dedup: bool) -> Self {
self.dedup = dedup;
self
}
pub fn scan(&self) -> impl Iterator<Item = Result<WeightedEntry>> + '_ {
debug!(
sources = self.sources.len(),
dedup = self.dedup,
"Gatherer::scan starting",
);
let mut seen_targets: HashSet<String> = HashSet::new();
let mut seen_aliased_lnks: HashSet<(String, String)> = HashSet::new();
let dedup = self.dedup;
self.sources
.iter()
.flat_map(|(src, prio)| {
let prio = *prio;
let origin = src.origin();
debug!(origin = %origin, "scanning source");
let traced = TracedSourceIter::new(src.scan(), origin.clone());
traced.map(move |r| (r, prio, origin.clone()))
})
.filter_map(move |(result, prio, origin)| match result {
Ok(entry) => {
let keep = !dedup
|| dedup_decision(
&mut seen_targets,
&mut seen_aliased_lnks,
entry.path(),
entry.link_path(),
);
keep.then(|| {
Ok(WeightedEntry {
entry,
origin,
source_priority: prio,
index: OnceLock::new(),
})
})
}
Err(err) => Some(Err(err)),
})
}
}
struct TracedSourceIter<I> {
inner: I,
origin: Origin,
started: Instant,
ok: u64,
err: u64,
}
impl<I> TracedSourceIter<I> {
fn new(inner: I, origin: Origin) -> Self {
Self {
inner,
origin,
started: Instant::now(),
ok: 0,
err: 0,
}
}
}
impl<I> Iterator for TracedSourceIter<I>
where
I: Iterator<Item = Result<Box<dyn FileEntry>>>,
{
type Item = Result<Box<dyn FileEntry>>;
fn next(&mut self) -> Option<Self::Item> {
let item = self.inner.next()?;
match &item {
Ok(_) => self.ok += 1,
Err(_) => self.err += 1,
}
Some(item)
}
}
impl<I> Drop for TracedSourceIter<I> {
fn drop(&mut self) {
debug!(
origin = %self.origin,
ok = self.ok,
err = self.err,
elapsed_ms = self.started.elapsed().as_millis() as u64,
"source scan finished",
);
}
}