Skip to content

Repository files navigation

Voy

Caution

Pre-alpha

Voy is a lightweight, embeddable filesystem event pipeline and multi-route orchestrator. Similar to tools like Watchman, Watchexec, and entr, Voy watches files for events and optionally executes commands in response to those events. Where Voy shines is:

  • Small footprint
  • Fully embeddable
  • Foreground or background (daemon) command execution
  • Multi-route orchestrator for more complex needs
  • Process group isolation

Feature Matrix

Feature Voy Watchman Watchexec entr
Embeddable Yes No Yes No
Foreground Execution Yes No Yes Yes
Background Execution1 Yes Yes No No
Multi-Route Orchestration Yes Yes No No
Built-in Globbing Yes Yes Yes No
Environment Variable Injection Yes Yes Yes No
Recursive Traversal Yes Yes Yes No
Dynamic Directory Watching Yes Yes Yes No
Multi-OS Support No Yes Yes Yes
Naive Polling (Fallback) No Yes Yes Yes

Installation

Voy requires a C++23 compliant compiler (e.g., GCC 14+ or Clang 18+). To build and install:

git clone https://github.com/mharrisb1/voy.git
cd voy
make install

CLI Usage

To use Voy from the command line, create a .voy.json (or .yaml) configuration file in the root of your project. This file defines the routes and commands you want to execute when files change.

Example .voy.json

{
  "debounce_ms": 150,
  "routes": [
    {
      "name": "compile_project",
      "watch": ["src/**/*.cpp", "include/**/*.hpp"],
      "ignore": ["build/**"],
      "events": ["modify", "create", "delete"],
      "action": {
        "command": ["make", "build"],
        "workdir": ".",
        "grace_period_ms": 2000,
        "env": {
          "BUILD_ENV": "development"
        }
      }
    }
  ]
}

Note on Globs: Voy uses a lightweight, custom string matching engine that follows .gitignore semantics.

Supported:

  • ** : Recursive directory matching (e.g., src/**/*.cpp)
  • * : Any sequence of characters within a single directory (e.g., src/*.cpp)
  • ? : Any single character (e.g., test_?.cpp)
  • [...] : Bracket character classes, including ranges and negation (e.g., [a-z]*.cpp, [!0-9]*)
  • Leading / : Anchor the match to the root of the project (e.g., /build/)
  • Trailing / : Match directories only (e.g., build/)

Not Supported (Escaped as literals):

  • Brace expansion (e.g., *.{cpp,hpp}) - you must specify these as separate watch rules.
  • Extended regex syntax (e.g., +, (), |).

Running Voy

Start the file watcher in the foreground. By default, it looks for a .voy.json file in the current directory:

voy watch

You can also specify a custom configuration file path using the --config flag:

voy -c configs/voy.json watch

Help (voy -h):

Usage: voy [OPTIONS] <COMMAND>

Naughty little file watcher

Commands:
  watch         Start the event loop in the foreground

Options:
  -h, --help                    Print this help and exit
  -c, --config <val>            Path to the config file (default: .voy.json)
  -f, --format <val>            Config format (json, yaml) (default: json)
  -r, --rootdir <val>           Root directory for watcher (default: .)
  --no-vcs-ignore               Don't load .gitignore
  --no-project-ignore           Don't load .ignore

Embedded Usage

The core of Voy is libvoy which is a zero-dependency embeddable filesystem event pipeline and multi-route orchestrator. With libvoy you can create your own file watching systems by leveraging all of the components that make Voy work.

Using libvoy over other options like chokidar or watchdog allows you to use a file watcher utility where interpreted languages, larger memory overhead, and garbage collection are prohibitively expensive. It also shines when integrated in existing C++ pipelines.

#include <chrono>
#include <vector>

#include <voy/voy.h>

namespace Renderer {
  void compile_and_inject_shader(const std::string& shader_path) {...}
}

int main() {
  auto engine = voy::Engine::builder()
    .with_debounce_window(std::chrono::milliseconds(10))
    .on_stdout([](std::string_view chunk) {
      std::cout << chunk;
    })
    .on_stderr([](std::string_view chunk) {
      std::cerr << "\033[31m[voy error]\033[0m " << chunk;
    })
    .add_route(voy::Route::builder("shader_hot_reload")
      .watch("assets/shaders/**/*.frag")
      .watch("assets/shaders/**/*.vert")
      .on_events(voy::EventType::Modify)
      .with_callback([](const std::vector<voy::Event>& events) {
        for (const auto& evt : events) {
          Renderer::compile_and_inject_shader(evt.path.string());
        }
      })
      .build())
    .build();

  if (engine) {
    engine->run();
  }
}

Environment Variable Parameters

Warning

Volatile before stable release

For each process created by Voy, the following environment variables are passed in:

Name Description
VOY_ROUTE_NAME Optional name of route that handled the event
VOY_BATCH_SIZE Number of unique events collected in debounce window
VOY_EVENT_TYPE Type of event
VOY_EVENT_TIME Original timestamp of the event (ISO)
VOY_EVENT_PATH Absolute path of file or subdirectory

A Note on Batches

If more than one event is observed and handled within the debounce window then VOY_EVENT_PATH will be a delimited list of paths (the system's path separator, :, will be used). The VOY_BATCH_SIZE will let you know how many events were in the batch. The event types will also be OR'd so that if one path was associated with a modify event and another was associated with created then the reported VOY_EVENT_TYPE will be modify|created.

Footnotes

  1. https://github.com/mharrisb1/voy/issues/1

About

A naughty little file watcher

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages