Skip to content

Commit

Permalink
Implement --debounce and --poll
Browse files Browse the repository at this point in the history
  • Loading branch information
9999years committed Jul 7, 2023
1 parent 31b98e3 commit 8ab9405
Show file tree
Hide file tree
Showing 9 changed files with 151 additions and 21 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ categories = ["command-line-utilities", "development-tools"]
aho-corasick = "1.0.2"
camino = "1.1.4"
clap = { version = "4.3.2", features = ["derive", "wrap_help", "env"] }
# humantime = "2.1.0"
humantime = "2.1.0"
itertools = "0.11.0"
miette = { version = "5.9.0", features = ["fancy"] }
nix = { version = "0.26.2", default_features = false, features = ["process"] }
Expand Down
4 changes: 2 additions & 2 deletions src/clap_camino.rs → src/clap/camino.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl TypedValueParser for Utf8PathBufValueParser {
clap::Error::raw(
clap::error::ErrorKind::InvalidUtf8,
format!("Path isn't UTF-8: {path_buf:?}"),
)
).with_cmd(cmd)
})
})
}
Expand All @@ -36,6 +36,6 @@ impl ValueParserFactory for Utf8PathBufValueParserFactory {
type Parser = Utf8PathBufValueParser;

fn value_parser() -> Self::Parser {
Utf8PathBufValueParser::default()
Self::Parser::default()
}
}
91 changes: 91 additions & 0 deletions src/clap/humantime.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//! Adapter for parsing [`Duration`] with a [`clap::builder::Arg::value_parser`].

use std::time::Duration;

use clap::builder::StringValueParser;
use clap::builder::TypedValueParser;
use clap::builder::ValueParserFactory;
use humantime::DurationError;
use miette::LabeledSpan;
use miette::MietteDiagnostic;
use miette::Report;

/// Adapter for parsing [`Duration`] with a [`clap::builder::Arg::value_parser`].
#[derive(Default, Clone)]
pub struct DurationValueParser {
inner: StringValueParser,
}

impl TypedValueParser for DurationValueParser {
type Value = Duration;

fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
self.inner.parse_ref(cmd, arg, value).and_then(|str_value| {
humantime::parse_duration(&str_value).map_err(|err| {
let diagnostic = Report::new(MietteDiagnostic {
message: match &err {
DurationError::InvalidCharacter(_) => "Invalid character".to_owned(),
DurationError::NumberExpected(_) => "Expected number".to_owned(),
DurationError::UnknownUnit { unit, .. } => format!("Unknown unit `{unit}`"),
DurationError::NumberOverflow => "Duration is too long".to_owned(),
DurationError::Empty => "No duration given".to_owned(),
},
code: None,
severity: None,
help: match &err {
DurationError::InvalidCharacter(_) => {
Some("Non-alphanumeric characters are prohibited".to_owned())
}
DurationError::NumberExpected(_) => {
Some("Did you split a unit into multiple words?".to_owned())
}
DurationError::UnknownUnit { .. } => Some(
"Valid units include `ms` (milliseconds) and `s` (seconds)".to_owned(),
),
DurationError::NumberOverflow => None,
DurationError::Empty => None,
},
url: None,
labels: match err {
DurationError::InvalidCharacter(offset) => Some(vec![LabeledSpan::at(
offset..offset + 1,
"Invalid character",
)]),
DurationError::NumberExpected(offset) => {
Some(vec![LabeledSpan::at(offset..offset + 1, "Expected number")])
}
DurationError::UnknownUnit {
start,
end,
unit: _,
value: _,
} => Some(vec![LabeledSpan::at(start..end, "Unknown unit")]),
DurationError::NumberOverflow => None,
DurationError::Empty => None,
},
})
.with_source_code(str_value);
clap::Error::raw(
clap::error::ErrorKind::ValueValidation,
format!("{diagnostic:?}"),
)
.with_cmd(cmd)
})
})
}
}

struct DurationValueParserFactory;

impl ValueParserFactory for DurationValueParserFactory {
type Parser = DurationValueParser;

fn value_parser() -> Self::Parser {
Self::Parser::default()
}
}
6 changes: 6 additions & 0 deletions src/clap/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//! Adapters for parsing [`clap`] arguments to various types.

mod camino;
mod humantime;

pub use self::humantime::DurationValueParser;
38 changes: 24 additions & 14 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
//! To access arguments at any point in the program, use [`with_opts`] or [`with_opts_mut`].

use std::sync::Mutex;
use std::time::Duration;

use camino::Utf8PathBuf;
use clap::Parser;
Expand All @@ -24,16 +25,16 @@ pub struct Opts {
#[arg(long)]
pub command: Option<String>,

/// A path to watch for changes. Can be given multiple times.
#[arg(long)]
pub watch: Vec<Utf8PathBuf>,

/// A shell command which runs tests. If given, this command will be run after reloads.
///
/// May contain quoted arguments which will be parsed in a `sh`-like manner.
#[arg(long)]
pub test: Option<String>,

/// Options to modify file watching.
#[command(flatten)]
pub watch: WatchOpts,

/// Options to modify logging and error-handling behavior.
#[command(flatten)]
pub logging: LoggingOpts,
Expand All @@ -43,19 +44,28 @@ pub struct Opts {
#[derive(Debug, Clone, clap::Args)]
#[clap(next_help_heading = "File watching options")]
pub struct WatchOpts {
/// Use polling rather than notification-based file watching. Polling tends to be more reliable
/// and less performant.
#[arg(long)]
pub poll: bool,
/// Use polling with the given interval rather than notification-based file watching. Polling
/// tends to be more reliable and less performant.
#[arg(long, value_name = "DURATION", value_parser = crate::clap::DurationValueParser::default())]
pub poll: Option<Duration>,

/// Debounce file events; wait this duration after receiving an event before attempting to
/// reload.
///
/// Defaults to 0.5 seconds.
///
/// TODO: Parse this into a duration.
#[arg(long, default_value = "500ms")]
pub debounce: String,
// Why do we need to use `value_parser` with this argument but not with the `Utf8PathBuf`
// arguments? I have no clue!
#[arg(
long,
default_value = "500ms",
value_name = "DURATION",
value_parser = crate::clap::DurationValueParser::default(),
)]
pub debounce: Duration,

/// A path to watch for changes. Can be given multiple times.
#[arg(long = "watch")]
pub paths: Vec<Utf8PathBuf>,
}

// TODO: Possibly set `RUST_LIB_BACKTRACE` from `RUST_BACKTRACE` as well, so that `full`
Expand Down Expand Up @@ -93,8 +103,8 @@ impl Opts {
///
/// Also sets environment variables according to the arguments.
pub fn set_opts(mut self) {
if self.watch.is_empty() {
self.watch.push("src".into());
if self.watch.paths.is_empty() {
self.watch.paths.push("src".into());
}

// These help our libraries (particularly `color-eyre`) see these options.
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

pub mod aho_corasick;
pub mod buffers;
pub mod clap_camino;
pub mod clap;
pub mod cli;
pub mod command;
pub mod event_filter;
Expand Down
9 changes: 8 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ async fn main() -> miette::Result<()> {
));

let ghci = Ghci::new(ghci_command).await?;
let watcher = cli::with_opts(|opts| Watcher::new(ghci, &opts.watch))?;
let watcher = cli::with_opts(|opts| {
Watcher::new(
ghci,
&opts.watch.paths,
opts.watch.debounce,
opts.watch.poll,
)
})?;

watcher.handle.await.into_diagnostic()??;

Expand Down
13 changes: 11 additions & 2 deletions src/watcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ pub struct Watcher {

impl Watcher {
/// Create a new [`Watcher`] from a [`Ghci`] session.
pub fn new(ghci: Arc<Mutex<Ghci>>, watch: &[Utf8PathBuf]) -> miette::Result<Self> {
pub fn new(
ghci: Arc<Mutex<Ghci>>,
watch: &[Utf8PathBuf],
debounce: Duration,
poll: Option<Duration>,
) -> miette::Result<Self> {
let mut init_config = InitConfig::default();
init_config.on_error(PrintDebug(std::io::stderr()));

Expand All @@ -45,9 +50,13 @@ impl Watcher {
let mut runtime_config = RuntimeConfig::default();
runtime_config
.pathset(watch)
.action_throttle(Duration::from_millis(500))
.action_throttle(debounce)
.on_action(action_handler);

if let Some(interval) = poll {
runtime_config.file_watcher(watchexec::fs::Watcher::Poll(interval));
}

let watcher = Watchexec::new(init_config, runtime_config.clone())?;

let watcher_handle = watcher.main();
Expand Down

0 comments on commit 8ab9405

Please sign in to comment.