use devicons::FileIcon; use directories::BaseDirs; use ignore::overrides::OverrideBuilder; use lazy_static::lazy_static; use std::path::PathBuf; use tokio::task::JoinHandle; use tracing::debug; use crate::channels::OnAir; use crate::entry::{Entry, PreviewCommand, PreviewType}; use television_fuzzy::matcher::{config::Config, injector::Injector, Matcher}; use television_utils::files::{walk_builder, DEFAULT_NUM_THREADS}; pub struct Channel { matcher: Matcher, icon: FileIcon, crawl_handle: JoinHandle<()>, } impl Channel { pub fn new() -> Self { let matcher = Matcher::new(Config::default().match_paths(true)); let base_dirs = BaseDirs::new().unwrap(); let crawl_handle = tokio::spawn(crawl_for_repos( base_dirs.home_dir().to_path_buf(), matcher.injector(), )); Channel { matcher, icon: FileIcon::from("git"), crawl_handle, } } } impl Default for Channel { fn default() -> Self { Self::new() } } lazy_static! { static ref PREVIEW_COMMAND: PreviewCommand = PreviewCommand { command: String::from( "cd {} && git log --pretty=medium --all --graph --color", ), delimiter: ":".to_string(), }; } impl OnAir for Channel { fn find(&mut self, pattern: &str) { self.matcher.find(pattern); } fn results(&mut self, num_entries: u32, offset: u32) -> Vec { self.matcher.tick(); self.matcher .results(num_entries, offset) .into_iter() .map(|item| { let path = item.matched_string; Entry::new( path.clone(), PreviewType::Command(PREVIEW_COMMAND.clone()), ) .with_name_match_ranges(item.match_indices) .with_icon(self.icon) }) .collect() } fn get_result(&self, index: u32) -> Option { self.matcher.get_result(index).map(|item| { let path = item.matched_string; Entry::new( path.clone(), PreviewType::Command(PREVIEW_COMMAND.clone()), ) .with_icon(self.icon) }) } fn result_count(&self) -> u32 { self.matcher.matched_item_count } fn total_count(&self) -> u32 { self.matcher.total_item_count } fn running(&self) -> bool { self.matcher.status.running } fn shutdown(&self) { debug!("Shutting down git repos channel"); self.crawl_handle.abort(); } } fn get_ignored_paths() -> Vec { let mut ignored_paths = Vec::new(); if let Some(base_dirs) = BaseDirs::new() { let home = base_dirs.home_dir(); #[cfg(target_os = "macos")] { ignored_paths.push(home.join("Library")); ignored_paths.push(home.join("Applications")); ignored_paths.push(home.join("Music")); ignored_paths.push(home.join("Pictures")); ignored_paths.push(home.join("Movies")); ignored_paths.push(home.join("Downloads")); ignored_paths.push(home.join("Public")); } #[cfg(target_os = "linux")] { ignored_paths.push(home.join(".cache")); ignored_paths.push(home.join(".config")); ignored_paths.push(home.join(".local")); ignored_paths.push(home.join(".thumbnails")); ignored_paths.push(home.join("Downloads")); ignored_paths.push(home.join("Public")); ignored_paths.push(home.join("snap")); ignored_paths.push(home.join(".snap")); } #[cfg(target_os = "windows")] { ignored_paths.push(home.join("AppData")); ignored_paths.push(home.join("Downloads")); ignored_paths.push(home.join("Documents")); ignored_paths.push(home.join("Music")); ignored_paths.push(home.join("Pictures")); ignored_paths.push(home.join("Videos")); } // Common paths to ignore for all platforms ignored_paths.push(home.join("node_modules")); ignored_paths.push(home.join("venv")); ignored_paths.push(PathBuf::from("/tmp")); } ignored_paths } #[allow(clippy::unused_async)] async fn crawl_for_repos(starting_point: PathBuf, injector: Injector) { let mut walker_overrides_builder = OverrideBuilder::new(&starting_point); walker_overrides_builder.add(".git").unwrap(); let walker = walk_builder( &starting_point, *DEFAULT_NUM_THREADS, Some(walker_overrides_builder.build().unwrap()), Some(get_ignored_paths()), ) .build_parallel(); walker.run(|| { let injector = injector.clone(); Box::new(move |result| { if let Ok(entry) = result { if entry.file_type().unwrap().is_dir() { // if the entry is a .git directory, add its parent to the list of git repos if entry.path().ends_with(".git") { let parent_path = &entry.path().parent().unwrap().to_string_lossy(); debug!("Found git repo: {:?}", parent_path); let () = injector.push( parent_path.to_string(), |e, cols| { cols[0] = e.clone().into(); }, ); return ignore::WalkState::Skip; } } } ignore::WalkState::Continue }) }); }