use std::{future::Future, time::Duration};
use tokio::sync::watch;
use tracing::{debug, trace};
use crate::{
action::{ActionHandler, ActionReturn},
changeable::{Changeable, ChangeableFn},
filter::{ChangeableFilterer, Filterer},
sources::fs::{WatchedPath, Watcher},
ErrorHook,
};
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct Config {
pub(crate) change_signal: watch::Sender<u64>,
pub action_handler: ChangeableFn<ActionHandler, ActionReturn>,
pub error_handler: ChangeableFn<ErrorHook, ()>,
pub pathset: Changeable<Vec<WatchedPath>>,
pub file_watcher: Changeable<Watcher>,
pub follow_symlinks: Changeable<bool>,
pub signal_job_control: bool,
pub keyboard_events: Changeable<bool>,
pub throttle: Changeable<Duration>,
pub filterer: ChangeableFilterer,
pub error_channel_size: usize,
pub event_channel_size: usize,
pub(crate) fs_ready: watch::Sender<()>,
}
impl Default for Config {
fn default() -> Self {
Self {
change_signal: watch::channel(0).0,
action_handler: ChangeableFn::new(ActionReturn::Sync),
error_handler: Default::default(),
pathset: Default::default(),
file_watcher: Default::default(),
follow_symlinks: Changeable::new(true),
signal_job_control: false,
keyboard_events: Default::default(),
throttle: Changeable::new(Duration::from_millis(50)),
filterer: Default::default(),
error_channel_size: 64,
event_channel_size: 4096,
fs_ready: watch::channel(()).0,
}
}
}
impl Config {
#[allow(
clippy::must_use_candidate,
reason = "this return can explicitly be ignored"
)]
pub fn signal_change(&self) -> &Self {
self.change_signal.send_modify(|revision| {
*revision = revision
.checked_add(1)
.expect("configuration revision overflow");
});
self
}
#[must_use]
pub(crate) fn watch(&self) -> ConfigWatched {
ConfigWatched::new(self.change_signal.subscribe())
}
#[must_use]
pub fn fs_ready(&self) -> watch::Receiver<()> {
self.fs_ready.subscribe()
}
pub fn pathset<I, P>(&self, pathset: I) -> &Self
where
I: IntoIterator<Item = P>,
P: Into<WatchedPath>,
{
let pathset = pathset.into_iter().map(std::convert::Into::into).collect();
debug!(?pathset, "Config: pathset");
self.pathset.replace(pathset);
self.signal_change()
}
pub fn file_watcher(&self, watcher: Watcher) -> &Self {
debug!(?watcher, "Config: file watcher");
self.file_watcher.replace(watcher);
self.signal_change()
}
pub fn follow_symlinks(&self, follow: bool) -> &Self {
debug!(?follow, "Config: follow symlinks");
self.follow_symlinks.replace(follow);
self.signal_change()
}
pub fn keyboard_events(&self, enable: bool) -> &Self {
debug!(?enable, "Config: keyboard");
self.keyboard_events.replace(enable);
self.signal_change()
}
pub fn throttle(&self, throttle: impl Into<Duration>) -> &Self {
let throttle = throttle.into();
debug!(?throttle, "Config: throttle");
self.throttle.replace(throttle);
self.signal_change()
}
pub fn filterer(&self, filterer: impl Filterer + 'static) -> &Self {
debug!(?filterer, "Config: filterer");
self.filterer.replace(filterer);
self.signal_change()
}
pub fn on_error(&self, handler: impl Fn(ErrorHook) + Send + Sync + 'static) -> &Self {
debug!("Config: on_error");
self.error_handler.replace(handler);
self.signal_change()
}
pub fn on_action(
&self,
handler: impl (Fn(ActionHandler) -> ActionHandler) + Send + Sync + 'static,
) -> &Self {
debug!("Config: on_action");
self.action_handler
.replace(move |action| ActionReturn::Sync(handler(action)));
self.signal_change()
}
pub fn on_action_async(
&self,
handler: impl (Fn(ActionHandler) -> Box<dyn Future<Output = ActionHandler> + Send + Sync>)
+ Send
+ Sync
+ 'static,
) -> &Self {
debug!("Config: on_action_async");
self.action_handler
.replace(move |action| ActionReturn::Async(handler(action)));
self.signal_change()
}
}
#[derive(Debug)]
pub(crate) struct ConfigWatched {
first_run: bool,
revision: watch::Receiver<u64>,
}
impl ConfigWatched {
const fn new(revision: watch::Receiver<u64>) -> Self {
Self {
first_run: true,
revision,
}
}
pub async fn next(&mut self) -> u64 {
if self.first_run {
let revision = *self.revision.borrow_and_update();
trace!(revision, "ConfigWatched: first run");
self.first_run = false;
revision
} else {
trace!("ConfigWatched: waiting for change");
self.revision
.changed()
.await
.expect("configuration change sender dropped");
let revision = *self.revision.borrow_and_update();
trace!(revision, "ConfigWatched: changed");
revision
}
}
pub fn pending(&self) -> bool {
self.first_run
|| self
.revision
.has_changed()
.expect("configuration change sender dropped")
}
}
#[cfg(test)]
mod tests {
use futures::FutureExt as _;
use super::Config;
#[test]
fn config_watch_first_run_is_immediate() {
let config = Config::default();
let mut watched = config.watch();
assert_eq!(watched.next().now_or_never(), Some(0));
}
#[test]
fn config_watch_waits_after_first_run() {
let config = Config::default();
let mut watched = config.watch();
assert!(watched.next().now_or_never().is_some());
assert!(watched.next().now_or_never().is_none());
}
#[test]
fn config_watch_observes_change_between_calls() {
let config = Config::default();
let mut watched = config.watch();
assert_eq!(watched.next().now_or_never(), Some(0));
config.signal_change();
assert_eq!(watched.next().now_or_never(), Some(1));
}
}