Implement debounce time for automatic reloading (#117)

This commit is contained in:
Max
2025-11-26 21:22:58 +01:00
committed by GitHub
parent 0a0112a7e1
commit 55e0c2b33f
3 changed files with 67 additions and 27 deletions
Generated
+7
View File
@@ -810,6 +810,12 @@ dependencies = [
"syn 2.0.111", "syn 2.0.111",
] ]
[[package]]
name = "debounce"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f2e5bc95e82bd8e9b333f4c5ff6dceab54e2e99f4d8cef2a680d417206ead34"
[[package]] [[package]]
name = "deltae" name = "deltae"
version = "0.3.2" version = "0.3.2"
@@ -3020,6 +3026,7 @@ dependencies = [
"criterion", "criterion",
"crossterm", "crossterm",
"csscolorparser 0.8.0", "csscolorparser 0.8.0",
"debounce",
"flexi_logger", "flexi_logger",
"flume", "flume",
"futures-util", "futures-util",
+1
View File
@@ -51,6 +51,7 @@ flexi_logger = "0.31"
# for tracing with tokio-console # for tracing with tokio-console
console-subscriber = { version = "0.5.0", optional = true } console-subscriber = { version = "0.5.0", optional = true }
debounce = "0.2.2"
[profile.production] [profile.production]
inherits = "release" inherits = "release"
+59 -27
View File
@@ -6,7 +6,10 @@ use std::{
borrow::Cow, borrow::Cow,
ffi::OsString, ffi::OsString,
io::{BufReader, Read as _, Stdout, Write as _, stdout}, io::{BufReader, Read as _, Stdout, Write as _, stdout},
path::PathBuf mem,
path::PathBuf,
sync::{Arc, Mutex},
time::Duration
}; };
use crossterm::{ use crossterm::{
@@ -17,6 +20,7 @@ use crossterm::{
enable_raw_mode, window_size enable_raw_mode, window_size
} }
}; };
use debounce::EventDebouncer;
use flexi_logger::FileSpec; use flexi_logger::FileSpec;
use flume::{Sender, r#async::RecvStream}; use flume::{Sender, r#async::RecvStream};
use futures_util::{FutureExt as _, stream::StreamExt as _}; use futures_util::{FutureExt as _, stream::StreamExt as _};
@@ -83,6 +87,8 @@ async fn inner_main() -> Result<(), WrappedErr> {
#[cfg(feature = "tracing")] #[cfg(feature = "tracing")]
console_subscriber::init(); console_subscriber::init();
const DEFAULT_DEBOUNCE_DELAY: Duration = Duration::from_millis(50);
let flags = xflags::parse_or_exit! { let flags = xflags::parse_or_exit! {
/// Display the pdf with the pages starting at the right hand size and moving left and /// Display the pdf with the pages starting at the right hand size and moving left and
/// adjust input keys to match /// adjust input keys to match
@@ -91,6 +97,9 @@ async fn inner_main() -> Result<(), WrappedErr> {
optional -m,--max-wide max_wide: NonZeroUsize optional -m,--max-wide max_wide: NonZeroUsize
/// Fullscreen the pdf (hide document name, page count, etc) /// Fullscreen the pdf (hide document name, page count, etc)
optional -f,--fullscreen 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 /// The number of pages to prerender surrounding the currently-shown page; 0 means no
/// limit. By default, there is no limit. /// limit. By default, there is no limit.
optional -p,--prerender prerender: usize optional -p,--prerender prerender: usize
@@ -162,7 +171,10 @@ async fn inner_main() -> Result<(), WrappedErr> {
watch_to_render_tx, watch_to_render_tx,
path.file_name() path.file_name()
.ok_or_else(|| WrappedErr("Path does not have a last component??".into()))? .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()))?; .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( fn on_notify_ev(
to_tui_tx: flume::Sender<Result<RenderInfo, RenderError>>, to_tui_tx: flume::Sender<Result<RenderInfo, RenderError>>,
to_render_tx: flume::Sender<RenderNotif>, to_render_tx: flume::Sender<RenderNotif>,
file_name: OsString file_name: OsString,
debounce_delay: Duration
) -> impl Fn(notify::Result<Event>) { ) -> impl Fn(notify::Result<Event>) {
move |res| match res { let last_event: Mutex<Result<(), RenderError>> = Mutex::new(Ok(()));
// If we get an error here, and then an error sending, everything's going wrong. Just give let last_event = Arc::new(last_event);
// 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 { let debouncer = EventDebouncer::new(debounce_delay, {
EventKind::Access(_) => (), let last_event = last_event.clone();
EventKind::Remove(_) => to_tui_tx move |()| {
.send(Err(RenderError::Converting("File was deleted".into()))) let event = mem::replace(&mut *last_event.lock().unwrap(), Ok(()));
.unwrap(), match event {
// This shouldn't fail to send unless the receiver gets disconnected. If that's // 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 // happened, then like the main thread has panicked or something, so it doesn't matter
// we don't handle the error here. // we don't handle the error here.
EventKind::Other | EventKind::Any | EventKind::Create(_) | EventKind::Modify(_) => Ok(()) => to_render_tx.send(RenderNotif::Reload).unwrap(),
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(());
} }
} }