#watcher

watchexec

Library to execute commands in response to file modifications

57 stable releases (6 major)

8.2.0 Mar 2, 2026
8.0.1 May 15, 2025
6.0.0 Feb 9, 2025
5.0.0 Oct 14, 2024
1.5.0 Nov 23, 2016

#36 in Filesystem

Download history 33907/week @ 2026-02-15 33806/week @ 2026-02-22 36538/week @ 2026-03-01 36390/week @ 2026-03-08 34879/week @ 2026-03-15 34595/week @ 2026-03-22 35944/week @ 2026-03-29 32361/week @ 2026-04-05 29847/week @ 2026-04-12 32098/week @ 2026-04-19 23665/week @ 2026-04-26 20281/week @ 2026-05-03 25081/week @ 2026-05-10 27113/week @ 2026-05-17 32378/week @ 2026-05-24 27164/week @ 2026-05-31

114,842 downloads per month
Used in 65 crates (30 directly)

Apache-2.0

240KB
5K SLoC

Crates.io page API Docs Crate license: Apache 2.0 CI status

Watchexec library

The library which powers Watchexec CLI and other tools.

Examples

Here's a complete example showing some of the library's features:

use miette::{IntoDiagnostic, Result};
use std::{
    sync::{Arc, Mutex},
    time::Duration,
};
use watchexec::{
    command::{Command, Program, Shell},
    job::CommandState,
    Watchexec,
};
use watchexec_events::{Event, Priority};
use watchexec_signals::Signal;

#[tokio::main]
async fn main() -> Result<()> {
    // this is okay to start with, but Watchexec logs a LOT of data,
    // even at error level. you will quickly want to filter it down.
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    // initialise Watchexec with a simple initial action handler
    let job = Arc::new(Mutex::new(None));
    let wx = Watchexec::new({
        let outerjob = job.clone();
        move |mut action| {
            let (_, job) = action.create_job(Arc::new(Command {
                program: Program::Shell {
                    shell: Shell::new("bash"),
                    command: "
                        echo 'Hello world'
                        trap 'echo Not quitting yet!' TERM
                        read
                    "
                    .into(),
                    args: Vec::new(),
                },
                options: Default::default(),
            }));

            // store the job outside this closure too
            *outerjob.lock().unwrap() = Some(job.clone());

            // block SIGINT
            #[cfg(unix)]
            job.set_spawn_hook(|cmd, _| {
                use nix::sys::signal::{sigprocmask, SigSet, SigmaskHow, Signal};
                unsafe {
                    cmd.command_mut().pre_exec(|| {
                        let mut newset = SigSet::empty();
                        newset.add(Signal::SIGINT);
                        sigprocmask(SigmaskHow::SIG_BLOCK, Some(&newset), None)?;
                        Ok(())
                    });
                }
            });

            // start the command
            job.start();

            action
        }
    })?;

    // start the engine
    let main = wx.main();

    // send an event to start
    wx.send_event(Event::default(), Priority::Urgent)
        .await
        .unwrap();
    // ^ this will cause the action handler we've defined above to run,
    //   creating and starting our little bash program, and storing it in the mutex

    // spin until we've got the job
    while job.lock().unwrap().is_none() {
        tokio::task::yield_now().await;
    }

    // watch the job and restart it when it exits
    let job = job.lock().unwrap().clone().unwrap();
    let auto_restart = tokio::spawn(async move {
        loop {
            job.to_wait().await;
            job.run(|context| {
                if let CommandState::Finished {
                    status,
                    started,
                    finished,
                } = context.current
                {
                    let duration = *finished - *started;
                    eprintln!("[Program stopped with {status:?}; ran for {duration:?}]")
                }
            })
            .await;

            eprintln!("[Restarting...]");
            job.start().await;
        }
    });

    // now we change what the action does:
    let auto_restart_abort = auto_restart.abort_handle();
    wx.config.on_action(move |mut action| {
        // if we get Ctrl-C on the Watchexec instance, we quit
        if action.signals().any(|sig| sig == Signal::Interrupt) {
            eprintln!("[Quitting...]");
            auto_restart_abort.abort();
            action.quit_gracefully(Signal::ForceStop, Duration::ZERO);
            return action;
        }

        // if the action was triggered by file events, gracefully stop the program
        if action.paths().next().is_some() {
            // watchexec can manage ("supervise") more than one program;
            // here we only have one but we don't know its Id so we grab it out of the iterator
            if let Some(job) = action.list_jobs().next().map(|(_, job)| job.clone()) {
                eprintln!("[Asking program to stop...]");
                job.stop_with_signal(Signal::Terminate, Duration::from_secs(5));
            }
        }

        action
    });

    // and watch all files in the current directory:
    wx.config.pathset(["."]);

    // then keep running until Watchexec quits!
    let _ = main.await.into_diagnostic()?;
    auto_restart.abort();
    Ok(())
}

Other examples:

Kitchen sink

Though not its primary usecase, the library exposes most of its relatively standalone components, available to make other tools that are not Watchexec-shaped:

Filterers are split into their own crates, so they can be evolved independently:

  • The Globset filterer implements the default Watchexec CLI filtering, based on the regex crate's ignore mechanisms.

  • The Tagged filterer was an experiment in creating a more powerful filtering solution, which could operate on every part of events, not just their paths, using a custom syntax. It is no longer maintained.

  • The Ignore filterer implements ignore-file semantics, and especially supports trees of ignore files. It is used as a subfilterer in both of the main filterers above.

There are also separate, standalone crates used to build Watchexec which you can tap into:

  • Supervisor is Watchexec's process supervisor and command abstraction.

  • ClearScreen makes clearing the terminal screen in a cross-platform way easy by default, and provides advanced options to fit your usecase.

  • Command Group augments the std and tokio Command with support for process groups, portable between Unix and Windows.

  • Event types contains the event types used by Watchexec, including the JSON format used for passing event data to child processes.

  • Signal types contains the signal types used by Watchexec.

  • Ignore files finds, parses, and interprets ignore files.

  • Project Origins finds the origin (or root) path of a project, and what kind of project it is.

Rust version (MSRV)

Due to the unpredictability of dependencies changing their MSRV, this library no longer tries to keep to a minimum supported Rust version behind stable. Instead, it is assumed that developers use the latest stable at all times.

Applications that wish to support lower-than-stable Rust (such as the Watchexec CLI does) should:

  • use a lock file
  • recommend the use of --locked when installing from source
  • provide pre-built binaries (and Binstall support) for non-distro users
  • avoid using newer features until some time has passed, to let distro users catch up
  • consider recommending that distro-Rust users switch to distro rustup where available

Dependencies

~12–28MB
~261K SLoC