From 791ad7251789cba5f9f0c61e598ebb9374636b67 Mon Sep 17 00:00:00 2001 From: alexpasmantier Date: Wed, 25 Sep 2024 20:28:18 +0200 Subject: [PATCH] making things async --- src/app.rs | 16 ++- src/components.rs | 4 +- src/components/channels.rs | 10 +- src/components/finders.rs | 3 +- src/components/finders/env.rs | 5 +- src/components/finders/files.rs | 166 ++++++++++++++++++++++---------- src/components/fps.rs | 91 ----------------- src/components/home.rs | 48 --------- src/components/pickers.rs | 2 +- src/components/pickers/env.rs | 16 ++- src/components/pickers/files.rs | 6 +- src/components/television.rs | 17 ++-- src/main.rs | 2 +- 13 files changed, 163 insertions(+), 223 deletions(-) delete mode 100644 src/components/fps.rs delete mode 100644 src/components/home.rs diff --git a/src/app.rs b/src/app.rs index fababe1..d24388e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -93,11 +93,11 @@ pub enum Mode { } impl App { - pub fn new(channel: UnitTvChannel, tick_rate: f64, frame_rate: f64) -> Result { + pub async fn new(channel: UnitTvChannel, tick_rate: f64, frame_rate: f64) -> Result { let (action_tx, action_rx) = mpsc::unbounded_channel(); let (render_tx, _) = mpsc::unbounded_channel(); let event_loop = EventLoop::new(tick_rate, true); - let television = Arc::new(Mutex::new(Television::new(channel))); + let television = Arc::new(Mutex::new(Television::new(channel).await)); Ok(Self { tick_rate, @@ -148,7 +148,7 @@ impl App { action_tx.send(action)?; } - self.handle_actions()?; + self.handle_actions().await?; if self.should_quit { self.event_abort_tx.send(())?; @@ -195,7 +195,7 @@ impl App { } } - fn handle_actions(&mut self) -> Result<()> { + async fn handle_actions(&mut self) -> Result<()> { while let Ok(action) = self.action_rx.try_recv() { if action != Action::Tick && action != Action::Render { debug!("{action:?}"); @@ -219,7 +219,13 @@ impl App { Action::Render => self.render_tx.send(RenderingTask::Render)?, _ => {} } - if let Some(action) = self.television.lock().unwrap().update(action.clone())? { + if let Some(action) = self + .television + .lock() + .unwrap() + .update(action.clone()) + .await? + { self.action_tx.send(action)? }; } diff --git a/src/components.rs b/src/components.rs index 565cf53..3dc3e62 100644 --- a/src/components.rs +++ b/src/components.rs @@ -9,8 +9,6 @@ use crate::{action::Action, config::Config}; pub mod channels; mod finders; -pub mod fps; -pub mod home; mod input; mod pickers; mod previewers; @@ -70,7 +68,7 @@ pub trait Component: Send { /// # Returns /// /// * `Result>` - An action to be processed or none. - fn update(&mut self, action: Action) -> Result> { + async fn update(&mut self, action: Action) -> Result> { let _ = action; // to appease clippy Ok(None) } diff --git a/src/components/channels.rs b/src/components/channels.rs index 98313d1..7f61fed 100644 --- a/src/components/channels.rs +++ b/src/components/channels.rs @@ -11,10 +11,10 @@ pub enum TvChannel { } impl TvChannel { - pub fn load_entries(&mut self, pattern: &str) -> Result<()> { + pub async fn load_entries(&mut self, pattern: &str) -> Result<()> { match self { - TvChannel::Env(picker) => picker.load_entries(pattern), - TvChannel::Files(picker) => picker.load_entries(pattern), + TvChannel::Env(picker) => picker.load_entries(pattern).await, + TvChannel::Files(picker) => picker.load_entries(pattern).await, } } @@ -43,10 +43,10 @@ impl TvChannel { } } -pub fn get_tv_channel(channel: UnitTvChannel) -> TvChannel { +pub async 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()), + UnitTvChannel::FILES => TvChannel::Files(pickers::files::FilePicker::new().await), _ => unimplemented!(), } } diff --git a/src/components/finders.rs b/src/components/finders.rs index eb11e92..297b8ee 100644 --- a/src/components/finders.rs +++ b/src/components/finders.rs @@ -1,3 +1,4 @@ +use futures::Stream; use rust_devicons::FileIcon; mod env; @@ -42,5 +43,5 @@ pub const ENTRY_PLACEHOLDER: Entry = Entry { /// # Methods /// - `find`: Find entries based on a pattern. pub trait Finder { - fn find(&mut self, pattern: &str) -> impl Iterator; + async fn find(&mut self, pattern: &str) -> impl Stream; } diff --git a/src/components/finders/env.rs b/src/components/finders/env.rs index f03b0ca..3a5b93f 100644 --- a/src/components/finders/env.rs +++ b/src/components/finders/env.rs @@ -1,5 +1,6 @@ use std::{cmp::max, collections::HashMap, env::vars, path::Path}; +use futures::{stream, Stream}; use fuzzy_matcher::skim::SkimMatcherV2; use rust_devicons::{icon_for_file, File, FileIcon}; @@ -34,7 +35,7 @@ impl EnvVarFinder { } impl Finder for EnvVarFinder { - fn find(&mut self, pattern: &str) -> impl Iterator { + async fn find(&mut self, pattern: &str) -> impl Stream { let mut results: Vec = Vec::new(); // try to get from cache if let Some(entries) = self.cache.get(pattern) { @@ -87,6 +88,6 @@ impl Finder for EnvVarFinder { // cache the results self.cache.insert(pattern.to_string(), results.clone()); } - results.into_iter() + stream::iter(results) } } diff --git a/src/components/finders/files.rs b/src/components/finders/files.rs index e00df6f..4fb9468 100644 --- a/src/components/finders/files.rs +++ b/src/components/finders/files.rs @@ -1,3 +1,4 @@ +use futures::{stream, StreamExt}; use std::{ collections::HashMap, path::{Path, PathBuf}, @@ -5,6 +6,8 @@ use std::{ use fuzzy_matcher::skim::SkimMatcherV2; use ignore::{types::TypesBuilder, WalkBuilder}; +use tokio::sync::mpsc; +use tracing::info; use crate::{ components::finders::{Entry, Finder}, @@ -19,8 +22,8 @@ pub struct FileFinder { } impl FileFinder { - pub fn new() -> Self { - let files = load_files(&std::env::current_dir().unwrap()); + pub async fn new() -> Self { + let files = load_files(&std::env::current_dir().unwrap()).await; FileFinder { current_directory: std::env::current_dir().unwrap(), files, @@ -30,67 +33,98 @@ impl FileFinder { } } +/// TODO: we might need to tweak this a bit -> read the paper associated to the matcher to get +/// a sense of what the threshold should be const FUZZY_THRESHOLD: i64 = 2; impl Finder for FileFinder { - fn find(&mut self, pattern: &str) -> impl Iterator { + //async 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 rel_path = file.strip_prefix(&self.current_directory).unwrap(); + // let rel_path_str = rel_path.to_string_lossy(); + // if !pattern.is_empty() { + // if let Some((score, indices)) = self.matcher.fuzzy(&rel_path_str, pattern, true) + // { + // if score < FUZZY_THRESHOLD { + // continue; + // } + // results.push(Entry { + // name: rel_path_str.to_string(), + // 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() + //} + + async fn find(&mut self, pattern: &str) -> impl stream::Stream { let mut results: Vec = Vec::new(); - // try to get from cache + + // Try to get from cache asynchronously if let Some(entries) = self.cache.get(pattern) { results.extend(entries.iter().cloned()); } else { - for file in &self.files { - let rel_path = file.strip_prefix(&self.current_directory).unwrap(); - let rel_path_str = rel_path.to_string_lossy(); - if !pattern.is_empty() { - if let Some((score, indices)) = self.matcher.fuzzy(&rel_path_str, pattern, true) - { - if score < FUZZY_THRESHOLD { - continue; + let entries = stream::iter(self.files) + .filter_map(|file| { + let rel_path = file.strip_prefix(&self.current_directory).ok()?; + let rel_path_str = rel_path.to_string_lossy().to_string(); + let matcher = self.matcher.clone(); // Clone matcher to use inside async tasks + let pattern = pattern.to_string(); // Clone pattern for async task + + // Spawn async block for each file + async move { + if !pattern.is_empty() { + if let Some((score, indices)) = + matcher.fuzzy(&rel_path_str, &pattern, true) + { + if score >= FUZZY_THRESHOLD { + // If score is acceptable, return the Entry + return Some(Entry { + name: rel_path_str, + 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, + }); + } + } } - results.push(Entry { - name: rel_path_str.to_string(), - 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, - }); + None } - } - } + }) + .buffer_unordered(10) // Limit concurrent tasks + .collect::>>() + .await; + + results.extend(entries.into_iter().filter_map(|entry| entry)); self.cache.insert(pattern.to_string(), results.clone()); } - results.into_iter() + + stream::iter(results) // Return the results as a Stream } } -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 { - 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 +lazy_static::lazy_static! { + static ref DEFAULT_NUM_THREADS: usize = default_num_threads().into(); } fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder { @@ -104,3 +138,37 @@ fn walk_builder(path: &Path, n_threads: usize) -> WalkBuilder { builder.threads(n_threads); builder } + +async fn load_files(path: &Path) -> Vec { + let (tx, mut rx) = mpsc::channel(100); + let path = path.to_owned(); + + // Spawn a blocking task for the file walker + tokio::task::spawn_blocking(move || { + let walker = WalkBuilder::new(path) + .threads(*DEFAULT_NUM_THREADS) + .build_parallel(); + + walker.run(|| { + let tx = tx.clone(); + Box::new(move |result| { + if let Ok(entry) = result { + if entry.file_type().unwrap().is_file() { + // Send the path via the async channel + let _ = tx.blocking_send(entry.path().to_path_buf()); + } + } + ignore::WalkState::Continue + }) + }); + }); + + // Collect the files asynchronously + let mut files = Vec::new(); + while let Some(file) = rx.recv().await { + files.push(file); + } + info!("Loaded {} files", files.len()); + + files +} diff --git a/src/components/fps.rs b/src/components/fps.rs deleted file mode 100644 index a79c4b4..0000000 --- a/src/components/fps.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::time::Instant; - -use color_eyre::Result; -use ratatui::{ - layout::{Constraint, Layout, Rect}, - style::{Style, Stylize}, - text::Span, - widgets::Paragraph, - Frame, -}; - -use super::Component; - -use crate::action::Action; - -#[derive(Debug, Clone, PartialEq)] -pub struct FpsCounter { - last_tick_update: Instant, - tick_count: u32, - ticks_per_second: f64, - - last_frame_update: Instant, - frame_count: u32, - frames_per_second: f64, -} - -impl Default for FpsCounter { - fn default() -> Self { - Self::new() - } -} - -impl FpsCounter { - pub fn new() -> Self { - Self { - last_tick_update: Instant::now(), - tick_count: 0, - ticks_per_second: 0.0, - last_frame_update: Instant::now(), - frame_count: 0, - frames_per_second: 0.0, - } - } - - fn app_tick(&mut self) -> Result<()> { - self.tick_count += 1; - let now = Instant::now(); - let elapsed = (now - self.last_tick_update).as_secs_f64(); - if elapsed >= 1.0 { - self.ticks_per_second = self.tick_count as f64 / elapsed; - self.last_tick_update = now; - self.tick_count = 0; - } - Ok(()) - } - - fn render_tick(&mut self) -> Result<()> { - self.frame_count += 1; - let now = Instant::now(); - let elapsed = (now - self.last_frame_update).as_secs_f64(); - if elapsed >= 1.0 { - self.frames_per_second = self.frame_count as f64 / elapsed; - self.last_frame_update = now; - self.frame_count = 0; - } - Ok(()) - } -} - -impl Component for FpsCounter { - fn update(&mut self, action: Action) -> Result> { - match action { - Action::Tick => self.app_tick()?, - Action::Render => self.render_tick()?, - _ => {} - }; - Ok(None) - } - - fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - let [top, _] = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]).areas(area); - let message = format!( - "{:.2} ticks/sec, {:.2} FPS", - self.ticks_per_second, self.frames_per_second - ); - let span = Span::styled(message, Style::new().dim()); - let paragraph = Paragraph::new(span).right_aligned(); - frame.render_widget(paragraph, top); - Ok(()) - } -} diff --git a/src/components/home.rs b/src/components/home.rs deleted file mode 100644 index f6033da..0000000 --- a/src/components/home.rs +++ /dev/null @@ -1,48 +0,0 @@ -use color_eyre::Result; -use ratatui::{prelude::*, widgets::*}; -use tokio::sync::mpsc::UnboundedSender; - -use super::Component; -use crate::{action::Action, config::Config}; - -#[derive(Default)] -pub struct Home { - command_tx: Option>, - config: Config, -} - -impl Home { - pub fn new() -> Self { - Self::default() - } -} - -impl Component for Home { - fn register_action_handler(&mut self, tx: UnboundedSender) -> Result<()> { - self.command_tx = Some(tx); - Ok(()) - } - - fn register_config_handler(&mut self, config: Config) -> Result<()> { - self.config = config; - Ok(()) - } - - fn update(&mut self, action: Action) -> Result> { - match action { - Action::Tick => { - // add any logic here that should run on every tick - } - Action::Render => { - // add any logic here that should run on every render - } - _ => {} - } - Ok(None) - } - - fn draw(&mut self, frame: &mut Frame, area: Rect) -> Result<()> { - frame.render_widget(Paragraph::new("hello world"), area); - Ok(()) - } -} diff --git a/src/components/pickers.rs b/src/components/pickers.rs index 6866dbb..c744677 100644 --- a/src/components/pickers.rs +++ b/src/components/pickers.rs @@ -27,7 +27,7 @@ pub trait Picker { /// NOTE: this method could eventually be async /// Load entries based on a pattern. - fn load_entries(&mut self, pattern: &str) -> Result<()>; + async fn load_entries(&mut self, pattern: &str) -> Result<()>; /// Get all entries. fn entries(&self) -> &Vec; /// Clear all entries. diff --git a/src/components/pickers/env.rs b/src/components/pickers/env.rs index 812ee2a..cd314e1 100644 --- a/src/components/pickers/env.rs +++ b/src/components/pickers/env.rs @@ -11,10 +11,12 @@ pub struct EnvVarPicker { } impl EnvVarPicker { - pub fn new() -> Self { + pub async fn new() -> Self { + let mut finder = finders::EnvVarFinder::new(); + let entries = finder.find("").await.collect::>(); EnvVarPicker { - finder: finders::EnvVarFinder::new(), - entries: Vec::new(), + finder, + entries, previewer: previewers::EnvVarPreviewer::new(), } } @@ -24,8 +26,12 @@ impl Picker for EnvVarPicker { type F = finders::EnvVarFinder; type P = previewers::EnvVarPreviewer; - fn load_entries(&mut self, pattern: &str) -> Result<()> { - self.entries = self.finder.find(pattern).collect::>(); + async fn load_entries(&mut self, pattern: &str) -> Result<()> { + self.entries = self + .finder + .find(pattern) + .await + .collect::>(); self.entries.sort_by_key(|e| -e.score); Ok(()) } diff --git a/src/components/pickers/files.rs b/src/components/pickers/files.rs index e3d9e48..885c506 100644 --- a/src/components/pickers/files.rs +++ b/src/components/pickers/files.rs @@ -11,9 +11,9 @@ pub struct FilePicker { } impl FilePicker { - pub fn new() -> Self { + pub async fn new() -> Self { FilePicker { - finder: finders::FileFinder::new(), + finder: finders::FileFinder::new().await, entries: Vec::new(), previewer: previewers::FilePreviewer::new(), } @@ -24,7 +24,7 @@ impl Picker for FilePicker { type F = finders::FileFinder; type P = previewers::FilePreviewer; - fn load_entries(&mut self, pattern: &str) -> Result<()> { + async fn load_entries(&mut self, pattern: &str) -> Result<()> { self.entries = self.finder.find(pattern).collect::>(); self.entries.sort_by_key(|e| -e.score); Ok(()) diff --git a/src/components/television.rs b/src/components/television.rs index df47f4d..e74e164 100644 --- a/src/components/television.rs +++ b/src/components/television.rs @@ -53,9 +53,8 @@ pub struct Television { const EMPTY_STRING: &str = ""; impl Television { - pub fn new(channel: UnitTvChannel) -> Self { - let mut tv_channel = get_tv_channel(channel); - tv_channel.load_entries(EMPTY_STRING).unwrap(); + pub async fn new(channel: UnitTvChannel) -> Self { + let tv_channel = get_tv_channel(channel).await; let results = tv_channel.entries().clone(); Self { action_tx: None, @@ -71,8 +70,8 @@ impl Television { } } - fn sync_channel(&mut self, pattern: &str) -> Result<()> { - self.channel.load_entries(pattern)?; + async fn sync_channel(&mut self, pattern: &str) -> Result<()> { + self.channel.load_entries(pattern).await?; self.results_list.results = self.channel.entries().clone(); self.results_list.state.select(Some(0)); Ok(()) @@ -205,8 +204,8 @@ impl Television { } // UI size -const UI_WIDTH_PERCENT: u16 = 70; -const UI_HEIGHT_PERCENT: u16 = 70; +const UI_WIDTH_PERCENT: u16 = 90; +const UI_HEIGHT_PERCENT: u16 = 90; // Misc const FOUR_SPACES: &str = " "; @@ -235,7 +234,7 @@ impl Component for Television { Ok(()) } - fn update(&mut self, action: Action) -> Result> { + async fn update(&mut self, action: Action) -> Result> { match action { Action::GoToPaneUp => { self.move_to_pane_on_top(); @@ -271,7 +270,7 @@ impl Component for Television { let new_pattern = self.input.value().to_string(); if new_pattern != self.current_pattern { self.current_pattern = new_pattern.clone(); - self.sync_channel(&new_pattern)?; + self.sync_channel(&new_pattern).await?; } } _ => {} diff --git a/src/main.rs b/src/main.rs index e88c38f..59a4f5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ async fn main() -> Result<()> { crate::logging::init()?; let args = Cli::parse(); - let mut app = App::new(args.channel, args.tick_rate, args.frame_rate)?; + let mut app = App::new(args.channel, args.tick_rate, args.frame_rate).await?; app.run().await?; Ok(()) }