mirror of
https://github.com/alexpasmantier/television.git
synced 2025-06-06 11:35:25 +00:00
257 lines
8.8 KiB
Rust
257 lines
8.8 KiB
Rust
use devicons::FileIcon;
|
|
use nucleo::{
|
|
pattern::{CaseMatching, Normalization},
|
|
Config, Injector, Nucleo,
|
|
};
|
|
use std::{
|
|
fs::File,
|
|
io::{BufRead, Read, Seek},
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
|
|
use tracing::{debug, info};
|
|
|
|
use super::OnAir;
|
|
use crate::previewers::PreviewType;
|
|
use crate::utils::{
|
|
files::{is_not_text, walk_builder, DEFAULT_NUM_THREADS},
|
|
strings::preprocess_line,
|
|
};
|
|
use crate::{
|
|
entry::Entry, utils::strings::proportion_of_printable_ascii_characters,
|
|
};
|
|
use crate::{fuzzy::MATCHER, utils::strings::PRINTABLE_ASCII_THRESHOLD};
|
|
|
|
#[derive(Debug)]
|
|
struct CandidateLine {
|
|
path: PathBuf,
|
|
line: String,
|
|
line_number: usize,
|
|
}
|
|
|
|
impl CandidateLine {
|
|
fn new(path: PathBuf, line: String, line_number: usize) -> Self {
|
|
CandidateLine {
|
|
path,
|
|
line,
|
|
line_number,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::module_name_repetitions)]
|
|
pub struct Channel {
|
|
matcher: Nucleo<CandidateLine>,
|
|
last_pattern: String,
|
|
result_count: u32,
|
|
total_count: u32,
|
|
running: bool,
|
|
}
|
|
|
|
impl Channel {
|
|
pub fn new(working_dir: &Path) -> Self {
|
|
let matcher = Nucleo::new(Config::DEFAULT, Arc::new(|| {}), None, 1);
|
|
// start loading files in the background
|
|
tokio::spawn(load_candidates(
|
|
working_dir.to_path_buf(),
|
|
matcher.injector(),
|
|
));
|
|
Channel {
|
|
matcher,
|
|
last_pattern: String::new(),
|
|
result_count: 0,
|
|
total_count: 0,
|
|
running: false,
|
|
}
|
|
}
|
|
|
|
const MATCHER_TICK_TIMEOUT: u64 = 2;
|
|
}
|
|
|
|
impl Default for Channel {
|
|
fn default() -> Self {
|
|
Self::new(&std::env::current_dir().unwrap())
|
|
}
|
|
}
|
|
|
|
impl OnAir for Channel {
|
|
fn find(&mut self, pattern: &str) {
|
|
if pattern != self.last_pattern {
|
|
self.matcher.pattern.reparse(
|
|
0,
|
|
pattern,
|
|
CaseMatching::Smart,
|
|
Normalization::Smart,
|
|
pattern.starts_with(&self.last_pattern),
|
|
);
|
|
self.last_pattern = pattern.to_string();
|
|
}
|
|
}
|
|
|
|
fn result_count(&self) -> u32 {
|
|
self.result_count
|
|
}
|
|
|
|
fn total_count(&self) -> u32 {
|
|
self.total_count
|
|
}
|
|
|
|
fn results(&mut self, num_entries: u32, offset: u32) -> Vec<Entry> {
|
|
let status = self.matcher.tick(Self::MATCHER_TICK_TIMEOUT);
|
|
let snapshot = self.matcher.snapshot();
|
|
if status.changed {
|
|
self.result_count = snapshot.matched_item_count();
|
|
self.total_count = snapshot.item_count();
|
|
}
|
|
self.running = status.running;
|
|
let mut indices = Vec::new();
|
|
let mut matcher = MATCHER.lock();
|
|
|
|
snapshot
|
|
.matched_items(
|
|
offset
|
|
..(num_entries + offset)
|
|
.min(snapshot.matched_item_count()),
|
|
)
|
|
.map(move |item| {
|
|
snapshot.pattern().column_pattern(0).indices(
|
|
item.matcher_columns[0].slice(..),
|
|
&mut matcher,
|
|
&mut indices,
|
|
);
|
|
indices.sort_unstable();
|
|
indices.dedup();
|
|
let indices = indices.drain(..);
|
|
|
|
let line = item.matcher_columns[0].to_string();
|
|
let display_path =
|
|
item.data.path.to_string_lossy().to_string();
|
|
Entry::new(
|
|
display_path.clone() + &item.data.line_number.to_string(),
|
|
PreviewType::Files,
|
|
)
|
|
.with_display_name(display_path)
|
|
.with_value(line)
|
|
.with_value_match_ranges(indices.map(|i| (i, i + 1)).collect())
|
|
.with_icon(FileIcon::from(item.data.path.as_path()))
|
|
.with_line_number(item.data.line_number)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn get_result(&self, index: u32) -> Option<Entry> {
|
|
let snapshot = self.matcher.snapshot();
|
|
snapshot.get_matched_item(index).map(|item| {
|
|
let display_path = item.data.path.to_string_lossy().to_string();
|
|
Entry::new(display_path.clone(), PreviewType::Files)
|
|
.with_display_name(
|
|
display_path.clone()
|
|
+ ":"
|
|
+ &item.data.line_number.to_string(),
|
|
)
|
|
.with_icon(FileIcon::from(item.data.path.as_path()))
|
|
.with_line_number(item.data.line_number)
|
|
})
|
|
}
|
|
|
|
fn running(&self) -> bool {
|
|
self.running
|
|
}
|
|
|
|
fn shutdown(&self) {
|
|
todo!()
|
|
}
|
|
}
|
|
|
|
/// The maximum file size we're willing to search in.
|
|
///
|
|
/// This is to prevent taking humongous amounts of memory when searching in
|
|
/// a lot of files (e.g. starting tv in $HOME).
|
|
const MAX_FILE_SIZE: u64 = 4 * 1024 * 1024;
|
|
|
|
#[allow(clippy::unused_async)]
|
|
async fn load_candidates(path: PathBuf, injector: Injector<CandidateLine>) {
|
|
let current_dir = std::env::current_dir().unwrap();
|
|
let walker =
|
|
walk_builder(&path, *DEFAULT_NUM_THREADS, None).build_parallel();
|
|
|
|
walker.run(|| {
|
|
let injector = injector.clone();
|
|
let current_dir = current_dir.clone();
|
|
Box::new(move |result| {
|
|
if let Ok(entry) = result {
|
|
if entry.file_type().unwrap().is_file() {
|
|
if let Ok(m) = entry.metadata() {
|
|
if m.len() > MAX_FILE_SIZE {
|
|
return ignore::WalkState::Continue;
|
|
}
|
|
}
|
|
// iterate over the lines of the file
|
|
match File::open(entry.path()) {
|
|
Ok(file) => {
|
|
let mut reader = std::io::BufReader::new(&file);
|
|
let mut buffer = [0u8; 128];
|
|
match reader.read(&mut buffer) {
|
|
Ok(bytes_read) => {
|
|
if (bytes_read == 0)
|
|
|| is_not_text(&buffer)
|
|
.unwrap_or(false)
|
|
|| proportion_of_printable_ascii_characters(&buffer)
|
|
< PRINTABLE_ASCII_THRESHOLD
|
|
{
|
|
return ignore::WalkState::Continue;
|
|
}
|
|
reader
|
|
.seek(std::io::SeekFrom::Start(0))
|
|
.unwrap();
|
|
}
|
|
Err(_) => {
|
|
return ignore::WalkState::Continue;
|
|
}
|
|
}
|
|
let mut line_number = 0;
|
|
for maybe_line in reader.lines() {
|
|
match maybe_line {
|
|
Ok(l) => {
|
|
line_number += 1;
|
|
let line = preprocess_line(&l);
|
|
if line.is_empty() {
|
|
debug!("Empty line");
|
|
continue;
|
|
}
|
|
let candidate = CandidateLine::new(
|
|
entry
|
|
.path()
|
|
.strip_prefix(¤t_dir)
|
|
.unwrap()
|
|
.to_path_buf(),
|
|
line,
|
|
line_number,
|
|
);
|
|
let _ = injector.push(
|
|
candidate,
|
|
|c, cols| {
|
|
cols[0] =
|
|
c.line.clone().into();
|
|
},
|
|
);
|
|
}
|
|
Err(e) => {
|
|
info!("Error reading line: {:?}", e);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Err(e) => {
|
|
info!("Error opening file: {:?}", e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ignore::WalkState::Continue
|
|
})
|
|
});
|
|
}
|