From 4b80d875de73d95c4ba4137ca7e5f7362a7f602c Mon Sep 17 00:00:00 2001 From: Max Dexheimer Date: Wed, 26 Nov 2025 20:50:25 +0100 Subject: [PATCH] Implement debounce time for automatic reloading --- Cargo.lock | 7 +++++ Cargo.toml | 1 + src/main.rs | 88 ++++++++++++++++++++++++++++++++++++----------------- 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6b6a5f2..352e161 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -810,6 +810,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "debounce" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2e5bc95e82bd8e9b333f4c5ff6dceab54e2e99f4d8cef2a680d417206ead34" + [[package]] name = "deltae" version = "0.3.2" @@ -3020,6 +3026,7 @@ dependencies = [ "criterion", "crossterm", "csscolorparser 0.8.0", + "debounce", "flexi_logger", "flume", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 3240db2..ea13e6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ flexi_logger = "0.31" # for tracing with tokio-console console-subscriber = { version = "0.5.0", optional = true } +debounce = "0.2.2" [profile.production] inherits = "release" diff --git a/src/main.rs b/src/main.rs index d4013e9..b125cd9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,10 @@ use std::{ borrow::Cow, ffi::OsString, io::{BufReader, Read as _, Stdout, Write as _, stdout}, - path::PathBuf + mem, + path::PathBuf, + sync::{Arc, Mutex}, + time::Duration }; use crossterm::{ @@ -17,6 +20,7 @@ use crossterm::{ enable_raw_mode, window_size } }; +use debounce::EventDebouncer; use flexi_logger::FileSpec; use flume::{Sender, r#async::RecvStream}; use futures_util::{FutureExt as _, stream::StreamExt as _}; @@ -83,6 +87,8 @@ async fn inner_main() -> Result<(), WrappedErr> { #[cfg(feature = "tracing")] console_subscriber::init(); + const DEFAULT_DEBOUNCE_DELAY: Duration = Duration::from_millis(50); + let flags = xflags::parse_or_exit! { /// Display the pdf with the pages starting at the right hand size and moving left and /// adjust input keys to match @@ -91,6 +97,9 @@ async fn inner_main() -> Result<(), WrappedErr> { optional -m,--max-wide max_wide: NonZeroUsize /// Fullscreen the pdf (hide document name, page count, etc) optional -f,--fullscreen + /// The time to wait for the file to stop changing before reloading, in milliseconds. + /// Defaults to 50ms. + optional --reload-delay reload_delay: u64 /// The number of pages to prerender surrounding the currently-shown page; 0 means no /// limit. By default, there is no limit. optional -p,--prerender prerender: usize @@ -162,7 +171,10 @@ async fn inner_main() -> Result<(), WrappedErr> { watch_to_render_tx, path.file_name() .ok_or_else(|| WrappedErr("Path does not have a last component??".into()))? - .to_owned() + .to_owned(), + flags + .reload_delay + .map_or(DEFAULT_DEBOUNCE_DELAY, Duration::from_millis) )) .map_err(|e| WrappedErr(format!("Couldn't start watching the provided file: {e}").into()))?; @@ -454,38 +466,58 @@ async fn enter_redraw_loop( fn on_notify_ev( to_tui_tx: flume::Sender>, to_render_tx: flume::Sender, - file_name: OsString + file_name: OsString, + debounce_delay: Duration ) -> impl Fn(notify::Result) { - move |res| match res { - // If we get an error here, and then an error sending, everything's going wrong. Just give - // up lol. - Err(e) => to_tui_tx.send(Err(RenderError::Notify(e))).unwrap(), - // TODO: Should we match EventKind::Rename and propogate that so that the other parts of the - // process know that too? Or should that be - Ok(ev) => { - // We only watch the parent directory (see the comment above `watcher.watch` in `fn - // main`) so we need to filter out events to only ones that pertain to the single file - // we care about - if !ev - .paths - .iter() - .any(|path| path.file_name().is_some_and(|f| f == file_name)) - { - return; - } - - match ev.kind { - EventKind::Access(_) => (), - EventKind::Remove(_) => to_tui_tx - .send(Err(RenderError::Converting("File was deleted".into()))) - .unwrap(), + let last_event: Mutex> = Mutex::new(Ok(())); + let last_event = Arc::new(last_event); + + let debouncer = EventDebouncer::new(debounce_delay, { + let last_event = last_event.clone(); + move |()| { + let event = mem::replace(&mut *last_event.lock().unwrap(), Ok(())); + match event { // This shouldn't fail to send unless the receiver gets disconnected. If that's // happened, then like the main thread has panicked or something, so it doesn't matter // we don't handle the error here. - EventKind::Other | EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) => - to_render_tx.send(RenderNotif::Reload).unwrap(), + Ok(()) => to_render_tx.send(RenderNotif::Reload).unwrap(), + // If we get an error here, and then an error sending, everything's going wrong. Just give + // up lol. + Err(e) => to_tui_tx.send(Err(e)).unwrap() } } + }); + + move |res| { + let event = match res { + Err(e) => Err(RenderError::Notify(e)), + + // TODO: Should we match EventKind::Rename and propogate that so that the other parts of the + // process know that too? Or should that be + Ok(ev) => { + // We only watch the parent directory (see the comment above `watcher.watch` in `fn + // main`) so we need to filter out events to only ones that pertain to the single file + // we care about + if !ev + .paths + .iter() + .any(|path| path.file_name().is_some_and(|f| f == file_name)) + { + return; + } + + match ev.kind { + EventKind::Access(_) => return, + EventKind::Remove(_) => Err(RenderError::Converting("File was deleted".into())), + EventKind::Other + | EventKind::Any + | EventKind::Create(_) + | EventKind::Modify(_) => Ok(()) + } + } + }; + *last_event.lock().unwrap() = event; + debouncer.put(()); } }