From b7415d98ad1d42366bc54ea15a93e37d2a3b6c58 Mon Sep 17 00:00:00 2001 From: taro Date: Fri, 7 Feb 2025 01:27:11 -0300 Subject: [PATCH] feat(FileBrowser): use a thread for io --- src/actions/action.rs | 2 +- src/components/file_browser/file_browser.rs | 59 +++++++++++++++---- .../file_browser/keyboard_handler.rs | 2 +- src/components/file_browser/widget.rs | 22 +++++++ 4 files changed, 73 insertions(+), 12 deletions(-) diff --git a/src/actions/action.rs b/src/actions/action.rs index cc0fb14..9ed9fe0 100644 --- a/src/actions/action.rs +++ b/src/actions/action.rs @@ -221,7 +221,7 @@ impl Actions { pub fn to_file(&self) {} pub fn action_by_key(&self, key: KeyEvent) -> Vec { - log::debug!("action_by_key {key:?}"); + // log::debug!("action_by_key {key:?}"); if let KeyCode::Char(c) = key.code && key.modifiers.is_empty() diff --git a/src/components/file_browser/file_browser.rs b/src/components/file_browser/file_browser.rs index db7c396..ca69fc7 100644 --- a/src/components/file_browser/file_browser.rs +++ b/src/components/file_browser/file_browser.rs @@ -3,6 +3,13 @@ use std::{ collections::HashMap, path::PathBuf, rc::Rc, + sync::{ + mpsc::{channel, RecvTimeoutError}, + Arc, + Mutex, + }, + thread, + time::Duration, }; use crate::{ @@ -38,6 +45,8 @@ pub struct FileBrowser<'a> { pub(super) help: FileBrowserHelp<'a>, pub(super) focus_group: FocusGroup<'a>, + pub(super) files_from_io_thread: Arc>>, + pub(super) history: Rc>>, pub(super) current_directory: Rc, @@ -59,6 +68,40 @@ impl<'a> FileBrowser<'a> { let on_add_to_playlist_fn: Rc) + 'a>>>> = Rc::new(RefCell::new(None)); let add_mode = Rc::new(Cell::new(AddMode::AddToLibrary)); + let (io_thread, files_from_io_thread) = { + let (tx, rx) = channel::(); + let files_from_io_thread = Arc::new(Mutex::new(vec![])); + thread::spawn({ + let files_from_io_thread = Arc::clone(&files_from_io_thread); + move || loop { + let Ok(mut path) = rx.recv() else { + log::trace!("FileBrowser's IO thread will close now."); + break; + }; + log::trace!("FileBrowser's IO thread: received {path:?}"); + let path = loop { + match rx.recv_timeout(Duration::from_millis(100)) { + Ok(p) => { + log::trace!("FileBrowser's IO thread: debounced path {p:?}"); + path = p; + } + Err(RecvTimeoutError::Timeout) => { + log::trace!("FileBrowser's IO thread: will now process path {path:?}"); + break path; + } + _ => { + log::trace!("FileBrowser's IO thread (inner loop) will close now."); + return; + } + } + }; + let files = directory_to_songs_and_folders(path.as_path()); + *files_from_io_thread.lock().unwrap() = files; + } + }); + (tx, files_from_io_thread) + }; + children_list.line_style(|i| match i { FileBrowserSelection::Song(_) | FileBrowserSelection::CueSheet(_) => None, _ => Some(ratatui::style::Style::new().add_modifier(ratatui::style::Modifier::DIM)), @@ -143,18 +186,12 @@ impl<'a> FileBrowser<'a> { let children_list = Rc::new(children_list); parents_list.on_select({ let children_list = children_list.clone(); - let file_meta = file_meta.clone(); + // let file_meta = file_meta.clone(); move |item| { if let FileBrowserSelection::Directory(path) = item { - let files = directory_to_songs_and_folders(path.as_path()); - - if let Some(f) = files.first() { - file_meta.set_file(f.clone()); - } else { - file_meta.clear(); - } - - children_list.set_items(files); + if let Err(err) = io_thread.send(path.clone()) { + log::error!("FileBrowser: error sending path to IO thread {err:?}"); + }; } else { children_list.set_items(vec![]); } @@ -272,6 +309,8 @@ impl<'a> FileBrowser<'a> { file_meta, focus_group, + files_from_io_thread, + current_directory, on_enqueue_fn, on_add_to_lib_fn, diff --git a/src/components/file_browser/keyboard_handler.rs b/src/components/file_browser/keyboard_handler.rs index d753406..9f467bf 100644 --- a/src/components/file_browser/keyboard_handler.rs +++ b/src/components/file_browser/keyboard_handler.rs @@ -8,7 +8,7 @@ use super::{AddMode, FileBrowser}; impl OnActionMut for FileBrowser<'_> { fn on_action(&mut self, actions: Vec) { - log::debug!("FB action {actions:?}"); + // log::debug!("FB action {actions:?}"); if self.parents_list.filter().is_empty() && let Some(action) = actions.iter().find_map(|action| match action { diff --git a/src/components/file_browser/widget.rs b/src/components/file_browser/widget.rs index 7cf9c47..1ae0be5 100644 --- a/src/components/file_browser/widget.rs +++ b/src/components/file_browser/widget.rs @@ -35,6 +35,28 @@ impl Display for AddMode { impl WidgetRef for FileBrowser<'_> { fn render_ref(&self, area: Rect, buf: &mut Buffer) { + if let Ok(mut files_from_io_thread) = self.files_from_io_thread.try_lock() { + // If the IO thread has something for us, and the lock is free, let's grab that stuff. + // + // We use `try_lock` instead of `lock` to avoid blocking the UI while the other thread holds the lock. + // A responsive UI with stale data is better UX than a choppy UI. + // + // Conceptually, this code doesn't belong in a "render" method. + // We should probably have a "component.update()" method or something. + // In practice, it'd be the same — we'll do this in the same thread that + // processes input and does the rendering, between those two. + if !files_from_io_thread.is_empty() { + let files = std::mem::take(&mut *files_from_io_thread); + if let Some(f) = files.first() { + self.file_meta.set_file(f.clone()); + } else { + self.file_meta.clear(); + } + + self.children_list.set_items(files); + } + }; + let [area_top, area_main, _, area_help] = Layout::vertical([ Constraint::Length(2), Constraint::Min(10),