use {
color_eyre::Result,
rapidfuzz::fuzz,
std::{fs::File, slice, sync::Arc},
};
#[cfg(feature = "cli")]
pub mod history;
pub mod pools;
pub mod tags;
pub trait Entry: Clone + Send + Sync + for<'de> serde::Deserialize<'de> {
fn name(&self) -> &str;
fn desc(&self) -> Option<&str> {
None
}
}
#[derive(Debug)]
pub struct Buffer<T: Entry> {
ptr: *const T,
len: usize,
}
unsafe impl<T: Entry> Send for Buffer<T> {}
unsafe impl<T: Entry> Sync for Buffer<T> {}
impl<T: Entry> Drop for Buffer<T> {
fn drop(&mut self) {
if self.ptr.is_null() || self.len == 0 {
return;
}
unsafe {
let slice = slice::from_raw_parts_mut(self.ptr as *mut T, self.len);
drop(Box::from_raw(slice));
}
}
}
impl<T: Entry> Buffer<T> {
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline(always)]
pub unsafe fn iter(&self) -> impl Iterator<Item = &T> {
unsafe { slice::from_raw_parts(self.ptr, self.len).iter() }
}
}
#[derive(Clone, Debug)]
pub struct Db<T: Entry> {
pub buf: Arc<Buffer<T>>,
}
impl<T: Entry> Default for Db<T> {
fn default() -> Self {
Self {
buf: Arc::new(Buffer {
ptr: std::ptr::null(),
len: 0,
}),
}
}
}
impl<T: Entry> Db<T> {
pub fn from_csv(file_path: &str) -> Result<Self> {
let file = File::open(file_path)?;
let mut rdr = csv::Reader::from_reader(file);
let mut entries = Vec::new();
for res in rdr.deserialize() {
let entry: T = res?;
entries.push(entry);
}
let boxed: Box<[T]> = entries.into_boxed_slice();
let len = boxed.len();
let ptr = boxed.as_ptr();
let _ = Box::into_raw(boxed);
Ok(Self {
buf: Arc::new(Buffer { ptr, len }),
})
}
#[inline(always)]
fn lowercase(s: &str) -> String {
if s.is_ascii() {
let mut out = String::with_capacity(s.len());
unsafe {
let bytes = s.as_bytes();
let out_bytes = out.as_mut_vec();
out_bytes.extend(
bytes
.iter()
.map(|&b| if b.is_ascii_uppercase() { b + 32 } else { b }),
);
out_bytes.set_len(s.len());
}
out
} else {
s.to_lowercase()
}
}
pub fn search(&self, query: &str, limit: usize, sim_threshold: f64) -> Vec<String> {
let query_lower = Self::lowercase(query);
let mut matches: Vec<(f64, String)> = Vec::new();
unsafe {
for entry in self.buf.iter() {
let name_lower = Self::lowercase(entry.name());
let name_sim = fuzz::ratio(name_lower.chars(), query_lower.chars()) / 100.0;
let max_sim = if let Some(desc) = entry.desc() {
let desc_lower = Self::lowercase(desc);
let desc_sim = fuzz::ratio(desc_lower.chars(), query_lower.chars()) / 100.0;
name_sim.max(desc_sim)
} else {
name_sim
};
if max_sim > sim_threshold {
matches.push((max_sim, entry.name().to_string()));
}
}
}
matches.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
matches
.into_iter()
.take(limit)
.map(|(_, name)| name)
.collect()
}
pub fn autocomplete_with(&self, query: &str, limit: usize, threshold: f64) -> Vec<String> {
let query_lower = Self::lowercase(query);
let mut results = Vec::new();
unsafe {
for entry in self.buf.iter() {
let name_lower = Self::lowercase(entry.name());
let name_sim = fuzz::ratio(name_lower.chars(), query_lower.chars()) / 100.0;
if name_sim > threshold {
results.push(entry.name().to_string());
if results.len() >= limit {
break;
}
}
}
}
results
}
#[cfg(feature = "cli")]
pub fn autocomplete(&self, query: &str, limit: usize) -> Vec<String> {
self.autocomplete_with(query, limit, crate::getopt!(completion.tag_similarity_threshold))
}
pub fn exists(&self, name: &str) -> bool {
unsafe { self.buf.iter().any(|entry| entry.name() == name) }
}
pub fn get_by_name(&self, name: &str) -> Option<T> {
unsafe { self.buf.iter().find(|entry| entry.name() == name).cloned() }
}
}