From 4292573467964ef3208274153d9182bb58d8f60b Mon Sep 17 00:00:00 2001 From: Alexandre Pasmantier Date: Tue, 24 Sep 2024 00:17:32 +0200 Subject: [PATCH] start implementing a file channel --- Cargo.lock | 55 ++++++++++++++++ Cargo.toml | 1 + TODO.md | 24 +++---- src/cli.rs | 1 + src/components.rs | 2 +- src/components/channels.rs | 49 ++++++++++++++ src/components/finders.rs | 13 +++- src/components/finders/env.rs | 3 +- src/components/finders/files.rs | 101 +++++++++++++++++++++++++++++ src/components/input/backend.rs | 2 - src/components/pickers.rs | 47 ++------------ src/components/pickers/env.rs | 3 +- src/components/pickers/files.rs | 44 +++++++++++++ src/components/previewers.rs | 2 + src/components/previewers/env.rs | 14 +++- src/components/previewers/files.rs | 32 +++++++++ src/components/sorters.rs | 0 src/components/television.rs | 25 +++---- src/config.rs | 13 +++- 19 files changed, 347 insertions(+), 84 deletions(-) create mode 100644 src/components/channels.rs create mode 100644 src/components/finders/files.rs create mode 100644 src/components/pickers/files.rs create mode 100644 src/components/previewers/files.rs delete mode 100644 src/components/sorters.rs diff --git a/Cargo.lock b/Cargo.lock index dcc536f..4701297 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -432,6 +432,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crossterm" version = "0.28.1" @@ -1344,6 +1369,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -1413,6 +1451,22 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.7", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indenter" version = "0.3.3" @@ -2221,6 +2275,7 @@ dependencies = [ "futures", "fuzzy-matcher", "human-panic", + "ignore", "json5", "lazy_static", "libc", diff --git a/Cargo.toml b/Cargo.toml index d354092..562b061 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ directories = "5.0.1" futures = "0.3.30" fuzzy-matcher = "0.3.7" human-panic = "2.0.1" +ignore = "0.4.23" json5 = "0.4.1" lazy_static = "1.5.0" libc = "0.2.158" diff --git a/TODO.md b/TODO.md index 1c448b4..4fafa9c 100644 --- a/TODO.md +++ b/TODO.md @@ -1,18 +1,10 @@ -- separate event loop from tui -- translate events into actions inside the event handler loop -- have a dedicated rendering loop that can own the tui -- - - - - ## feature ideas -- environment variables -- aliases -- shell history -- grep (maybe also inside pdfs and other files (see rga)) -- fd -- recent directories -- git -- makefile commands +- [x] environment variables +- [ ] aliases +- [ ] shell history +- [ ] grep (maybe also inside pdfs and other files (see rga)) +- [ ] fd +- [ ] recent directories +- [ ] git +- [ ] makefile commands - diff --git a/src/cli.rs b/src/cli.rs index 9a8d325..4489179 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,7 @@ use crate::config::{get_config_dir, get_data_dir}; pub enum UnitTvChannel { #[default] ENV, + FILES, OTHER, } diff --git a/src/components.rs b/src/components.rs index d54b080..565cf53 100644 --- a/src/components.rs +++ b/src/components.rs @@ -7,13 +7,13 @@ use tokio::sync::mpsc::UnboundedSender; use crate::{action::Action, config::Config}; +pub mod channels; mod finders; pub mod fps; pub mod home; mod input; mod pickers; mod previewers; -mod sorters; pub mod television; mod utils; diff --git a/src/components/channels.rs b/src/components/channels.rs new file mode 100644 index 0000000..e7208f2 --- /dev/null +++ b/src/components/channels.rs @@ -0,0 +1,49 @@ +use color_eyre::Result; + +use crate::cli::UnitTvChannel; +use crate::components::finders::Entry; +use crate::components::pickers::{self, Picker}; +use crate::components::previewers; + +pub enum TvChannel { + Env(pickers::env::EnvVarPicker), + Files(pickers::files::FilePicker), +} + +impl TvChannel { + pub fn load_entries(&mut self, pattern: &str) -> Result<()> { + match self { + TvChannel::Env(picker) => picker.load_entries(pattern), + TvChannel::Files(picker) => picker.load_entries(pattern), + } + } + + pub fn entries(&self) -> &Vec { + match self { + TvChannel::Env(picker) => picker.entries(), + TvChannel::Files(picker) => picker.entries(), + } + } + + pub fn clear(&mut self) { + match self { + TvChannel::Env(picker) => picker.clear(), + TvChannel::Files(picker) => picker.clear(), + } + } + + pub fn get_preview(&mut self, entry: &Entry) -> previewers::Preview { + match self { + TvChannel::Env(picker) => picker.get_preview(entry), + TvChannel::Files(picker) => picker.get_preview(entry), + } + } +} + +pub fn get_tv_channel(channel: UnitTvChannel) -> TvChannel { + match channel { + UnitTvChannel::ENV => TvChannel::Env(pickers::env::EnvVarPicker::new()), + UnitTvChannel::FILES => TvChannel::Files(pickers::files::FilePicker::new()), + _ => unimplemented!(), + } +} diff --git a/src/components/finders.rs b/src/components/finders.rs index dfd03e0..eb11e92 100644 --- a/src/components/finders.rs +++ b/src/components/finders.rs @@ -1,12 +1,16 @@ +use rust_devicons::FileIcon; + mod env; +mod files; // finder types pub use env::EnvVarFinder; -use rust_devicons::FileIcon; +pub use files::FileFinder; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct Entry { pub name: String, + display_name: Option, pub preview: Option, pub score: i64, pub name_match_ranges: Option>, @@ -15,8 +19,15 @@ pub struct Entry { pub line_number: Option, } +impl Entry { + pub fn display_name(&self) -> &str { + self.display_name.as_ref().unwrap_or(&self.name) + } +} + pub const ENTRY_PLACEHOLDER: Entry = Entry { name: String::new(), + display_name: None, preview: None, score: 0, name_match_ranges: None, diff --git a/src/components/finders/env.rs b/src/components/finders/env.rs index a8ceff7..f03b0ca 100644 --- a/src/components/finders/env.rs +++ b/src/components/finders/env.rs @@ -2,7 +2,6 @@ use std::{cmp::max, collections::HashMap, env::vars, path::Path}; use fuzzy_matcher::skim::SkimMatcherV2; use rust_devicons::{icon_for_file, File, FileIcon}; -use tracing::info; use crate::components::finders::{Entry, Finder}; @@ -45,6 +44,7 @@ impl Finder for EnvVarFinder { if pattern.is_empty() { results.push(Entry { name: env_var.name.clone(), + display_name: None, preview: Some(env_var.value.clone()), score: 0, name_match_ranges: None, @@ -73,6 +73,7 @@ impl Finder for EnvVarFinder { if preview_match_ranges.is_some() || name_match_ranges.is_some() { results.push(Entry { name: env_var.name.clone(), + display_name: None, preview: Some(env_var.value.clone()), score: final_score, name_match_ranges, diff --git a/src/components/finders/files.rs b/src/components/finders/files.rs new file mode 100644 index 0000000..1e4ff11 --- /dev/null +++ b/src/components/finders/files.rs @@ -0,0 +1,101 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use fuzzy_matcher::skim::SkimMatcherV2; +use ignore::{types::TypesBuilder, WalkBuilder}; +use tracing::info; + +use crate::{ + components::finders::{Entry, Finder}, + config::default_num_threads, +}; + +pub struct FileFinder { + current_directory: PathBuf, + files: Vec, + matcher: SkimMatcherV2, + cache: HashMap>, +} + +impl FileFinder { + pub fn new() -> Self { + let files = load_files(&std::env::current_dir().unwrap()); + FileFinder { + current_directory: std::env::current_dir().unwrap(), + files, + matcher: SkimMatcherV2::default(), + cache: HashMap::new(), + } + } +} + +impl Finder for FileFinder { + fn find(&mut self, pattern: &str) -> impl Iterator { + let mut results: Vec = Vec::new(); + // try to get from cache + if let Some(entries) = self.cache.get(pattern) { + results.extend(entries.iter().cloned()); + } else { + for file in &self.files { + let file_name = file.file_name().unwrap().to_string_lossy().to_string(); + if !pattern.is_empty() { + if let Some((score, indices)) = self.matcher.fuzzy(&file_name, pattern, true) { + results.push(Entry { + name: file_name.clone(), + display_name: None, + preview: None, + score, + name_match_ranges: Some(indices.iter().map(|i| (*i, *i + 1)).collect()), + preview_match_ranges: None, + icon: None, + line_number: None, + }); + } + } + } + self.cache.insert(pattern.to_string(), results.clone()); + } + results.into_iter() + } +} + +const DEFAULT_RECV_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(250); + +fn load_files(path: &Path) -> Vec { + let (tx, rx) = std::sync::mpsc::channel(); + let walker = walk_builder(path, default_num_threads().into()).build_parallel(); + walker.run(|| { + let tx = tx.clone(); + Box::new(move |result| { + if let Ok(entry) = result { + info!("found file: {:?}", entry.path()); + if entry.file_type().unwrap().is_file() { + tx.send(entry.path().to_path_buf()).unwrap(); + } + ignore::WalkState::Continue + } else { + ignore::WalkState::Continue + } + }) + }); + + let mut files = Vec::new(); + while let Ok(file) = rx.recv_timeout(DEFAULT_RECV_TIMEOUT) { + files.push(file); + } + files +} + +fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder { + let mut builder = WalkBuilder::new(path); + + // ft-based filtering + let mut types_builder = TypesBuilder::new(); + types_builder.add_defaults(); + builder.types(types_builder.build().unwrap()); + + builder.threads(n_threads); + builder +} diff --git a/src/components/input/backend.rs b/src/components/input/backend.rs index 3c1810a..7b2e73b 100644 --- a/src/components/input/backend.rs +++ b/src/components/input/backend.rs @@ -1,5 +1,3 @@ -use crate::event::Key; - use super::{Input, InputRequest, StateChanged}; use ratatui::crossterm::event::{ Event as CrosstermEvent, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, diff --git a/src/components/pickers.rs b/src/components/pickers.rs index 7822962..6866dbb 100644 --- a/src/components/pickers.rs +++ b/src/components/pickers.rs @@ -1,10 +1,10 @@ -use crate::cli::UnitTvChannel; - -use super::finders; -use super::finders::Entry; -use super::previewers; use color_eyre::Result; + +use crate::components::finders::{self, Entry}; +use crate::components::previewers; + pub mod env; +pub mod files; /// A trait for a picker that can load entries, select an entry, get the selected entry, get all /// entries, clear the entries, and get a preview of the selected entry. @@ -35,40 +35,3 @@ pub trait Picker { /// Get a preview of the currently selected entry. fn get_preview(&mut self, entry: &Entry) -> previewers::Preview; } - -pub enum TvChannel { - Env(env::EnvVarPicker), -} - -impl TvChannel { - pub fn load_entries(&mut self, pattern: &str) -> Result<()> { - match self { - TvChannel::Env(picker) => picker.load_entries(pattern), - } - } - - pub fn entries(&self) -> &Vec { - match self { - TvChannel::Env(picker) => picker.entries(), - } - } - - pub fn clear(&mut self) { - match self { - TvChannel::Env(picker) => picker.clear(), - } - } - - pub fn get_preview(&mut self, entry: &Entry) -> previewers::Preview { - match self { - TvChannel::Env(picker) => picker.get_preview(entry), - } - } -} - -pub fn get_tv_channel(channel: UnitTvChannel) -> TvChannel { - match channel { - UnitTvChannel::ENV => TvChannel::Env(env::EnvVarPicker::new()), - _ => unimplemented!(), - } -} diff --git a/src/components/pickers/env.rs b/src/components/pickers/env.rs index 896ecac..812ee2a 100644 --- a/src/components/pickers/env.rs +++ b/src/components/pickers/env.rs @@ -1,6 +1,7 @@ use color_eyre::Result; use crate::components::finders::{self, Finder}; +use crate::components::pickers::Picker; use crate::components::previewers::{self, Previewer}; pub struct EnvVarPicker { @@ -19,7 +20,7 @@ impl EnvVarPicker { } } -impl super::Picker for EnvVarPicker { +impl Picker for EnvVarPicker { type F = finders::EnvVarFinder; type P = previewers::EnvVarPreviewer; diff --git a/src/components/pickers/files.rs b/src/components/pickers/files.rs new file mode 100644 index 0000000..e3d9e48 --- /dev/null +++ b/src/components/pickers/files.rs @@ -0,0 +1,44 @@ +use color_eyre::Result; + +use crate::components::finders::{self, Finder}; +use crate::components::pickers::Picker; +use crate::components::previewers::{self, Previewer}; + +pub struct FilePicker { + finder: finders::FileFinder, + entries: Vec, + previewer: previewers::FilePreviewer, +} + +impl FilePicker { + pub fn new() -> Self { + FilePicker { + finder: finders::FileFinder::new(), + entries: Vec::new(), + previewer: previewers::FilePreviewer::new(), + } + } +} + +impl Picker for FilePicker { + type F = finders::FileFinder; + type P = previewers::FilePreviewer; + + fn load_entries(&mut self, pattern: &str) -> Result<()> { + self.entries = self.finder.find(pattern).collect::>(); + self.entries.sort_by_key(|e| -e.score); + Ok(()) + } + + fn entries(&self) -> &Vec { + &self.entries + } + + fn clear(&mut self) { + self.entries.clear(); + } + + fn get_preview(&mut self, entry: &finders::Entry) -> previewers::Preview { + self.previewer.preview(entry) + } +} diff --git a/src/components/previewers.rs b/src/components/previewers.rs index dd51f19..8008f82 100644 --- a/src/components/previewers.rs +++ b/src/components/previewers.rs @@ -1,9 +1,11 @@ use super::finders::Entry; mod env; +mod files; // previewer types pub use env::EnvVarPreviewer; +pub use files::FilePreviewer; #[derive(Debug, Clone)] pub enum PreviewContent { diff --git a/src/components/previewers/env.rs b/src/components/previewers/env.rs index f1caaa4..177b498 100644 --- a/src/components/previewers/env.rs +++ b/src/components/previewers/env.rs @@ -24,7 +24,10 @@ impl Previewer for EnvVarPreviewer { let preview = Preview { title: entry.name.clone(), content: if let Some(preview) = &entry.preview { - PreviewContent::PlainTextWrapped(add_newline_after_colon(&preview)) + PreviewContent::PlainTextWrapped(maybe_add_newline_after_colon( + &preview, + &entry.name, + )) } else { PreviewContent::Empty }, @@ -34,6 +37,11 @@ impl Previewer for EnvVarPreviewer { } } -fn add_newline_after_colon(s: &str) -> String { - s.replace(":", ":\n") +const PATH: &str = "PATH"; + +fn maybe_add_newline_after_colon(s: &str, name: &str) -> String { + if name.contains(PATH) { + return s.replace(":", "\n"); + } + s.to_string() } diff --git a/src/components/previewers/files.rs b/src/components/previewers/files.rs new file mode 100644 index 0000000..6307d05 --- /dev/null +++ b/src/components/previewers/files.rs @@ -0,0 +1,32 @@ +use std::collections::HashMap; + +use crate::components::finders; +use crate::components::previewers::{Preview, PreviewContent, Previewer}; + +pub struct FilePreviewer { + cache: HashMap, +} + +impl FilePreviewer { + pub fn new() -> Self { + FilePreviewer { + cache: HashMap::new(), + } + } +} + +impl Previewer for FilePreviewer { + fn preview(&mut self, entry: &finders::Entry) -> Preview { + // check if we have that preview in the cache + if let Some(preview) = self.cache.get(entry) { + return preview.clone(); + } + let preview = Preview { + title: entry.name.clone(), + // TODO: add file preview + content: PreviewContent::PlainText(entry.name.clone()), + }; + self.cache.insert(entry.clone(), preview.clone()); + preview + } +} diff --git a/src/components/sorters.rs b/src/components/sorters.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/components/television.rs b/src/components/television.rs index bf31a2f..c856af0 100644 --- a/src/components/television.rs +++ b/src/components/television.rs @@ -1,4 +1,4 @@ -use color_eyre::{owo_colors::OwoColorize, Result}; +use color_eyre::Result; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Style, Stylize}, @@ -9,27 +9,20 @@ use ratatui::{ }, Frame, }; -use std::marker::PhantomData; use std::str::FromStr; -use tracing::{debug, info}; use tokio::sync::mpsc::UnboundedSender; -use crate::components::pickers::{env::EnvVarPicker, Picker}; -use crate::components::previewers::{Preview, PreviewContent, Previewer}; -use crate::components::utils::centered_rect; -use crate::components::Component; -use crate::{action::Action, config::Config}; -use crate::{ - cli::UnitTvChannel, - components::finders::{Entry, Finder}, -}; - -use super::{ +use crate::components::{ + channels::{get_tv_channel, TvChannel}, finders::ENTRY_PLACEHOLDER, - input::{backend::EventHandler, Input, InputRequest, StateChanged}, - pickers::{get_tv_channel, TvChannel}, + input::{Input, InputRequest, StateChanged}, + previewers::{Preview, PreviewContent}, + utils::centered_rect, + Component, }; +use crate::{action::Action, config::Config}; +use crate::{cli::UnitTvChannel, components::finders::Entry}; #[derive(PartialEq, Copy, Clone)] enum Pane { diff --git a/src/config.rs b/src/config.rs index b04e08e..c245431 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env, path::PathBuf}; +use std::{collections::HashMap, env, num::NonZeroUsize, path::PathBuf}; use color_eyre::Result; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; @@ -306,6 +306,17 @@ pub fn parse_key(raw: &str) -> Result { Ok(convert_raw_event_to_key(key_event)) } +pub fn default_num_threads() -> NonZeroUsize { + // default to 1 thread if we can't determine the number of available threads + let default = NonZeroUsize::MIN; + // never use more than 32 threads to avoid startup overhead + let limit = NonZeroUsize::new(32).unwrap(); + + std::thread::available_parallelism() + .unwrap_or(default) + .min(limit) +} + #[derive(Clone, Debug, Default, Deref, DerefMut)] pub struct Styles(pub HashMap>);