From 7cfdc137953e2001134fdb24af1f7575aca861c1 Mon Sep 17 00:00:00 2001 From: GigabiteStudios Date: Thu, 18 Jun 2026 19:22:59 -0500 Subject: [PATCH] feat(watch): add portable directory watching and timing --- CMakeLists.txt | 2 + src/directory_watcher.cpp | 96 +++++++++++++++++++++++++++++++++++++++ src/time.cpp | 13 ++++++ 3 files changed, 111 insertions(+) create mode 100644 src/directory_watcher.cpp create mode 100644 src/time.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b3b5d68..cdaab8e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,8 @@ add_library(iZo::izo ALIAS izo) target_sources(izo PRIVATE src/dialogs.cpp + src/directory_watcher.cpp + src/time.cpp $<$:src/dialogs_windows.cpp> $<$:src/dialogs_linux.cpp> ) diff --git a/src/directory_watcher.cpp b/src/directory_watcher.cpp new file mode 100644 index 0000000..1426f8e --- /dev/null +++ b/src/directory_watcher.cpp @@ -0,0 +1,96 @@ +#include + +#include +#include +#include +#include + +namespace izo { +namespace { + +using snapshot = std::map; + +snapshot scan(const directory_watch_options& options) { + snapshot files; + std::error_code ec; + auto add = [&](const std::filesystem::directory_entry& entry) { + if (!entry.is_regular_file(ec) || ec) { ec.clear(); return; } + const auto time = entry.last_write_time(ec); + if (!ec) files.emplace(entry.path(), time); + ec.clear(); + }; + if (options.recursive) { + for (std::filesystem::recursive_directory_iterator it( + options.path, std::filesystem::directory_options::skip_permission_denied, ec), end; + !ec && it != end; it.increment(ec)) add(*it); + } else { + for (std::filesystem::directory_iterator it( + options.path, std::filesystem::directory_options::skip_permission_denied, ec), end; + !ec && it != end; it.increment(ec)) add(*it); + } + return files; +} + +} // namespace + +struct directory_watcher::implementation { + directory_watch_options options; + file_event_callback callback; + std::atomic running{true}; + std::thread worker; + + implementation(directory_watch_options value, file_event_callback fn) + : options(std::move(value)), callback(std::move(fn)) { + worker = std::thread([this] { + auto previous = scan(options); + while (running.load(std::memory_order_relaxed)) { + std::this_thread::sleep_for(options.poll_interval); + if (!running.load(std::memory_order_relaxed)) break; + auto current = scan(options); + for (const auto& item : current) { + const auto old = previous.find(item.first); + if (old == previous.end()) callback({file_change::added, item.first}); + else if (old->second != item.second) callback({file_change::modified, item.first}); + } + for (const auto& item : previous) { + if (current.find(item.first) == current.end()) + callback({file_change::removed, item.first}); + } + previous = std::move(current); + } + }); + } + + ~implementation() { + running.store(false, std::memory_order_relaxed); + if (worker.joinable()) worker.join(); + } +}; + +directory_watcher::directory_watcher(std::unique_ptr implementation) noexcept + : implementation_(std::move(implementation)) {} +directory_watcher::~directory_watcher() = default; +directory_watcher::directory_watcher(directory_watcher&&) noexcept = default; +directory_watcher& directory_watcher::operator=(directory_watcher&&) noexcept = default; +void directory_watcher::stop() noexcept { implementation_.reset(); } + +directory_watcher watch_directory(const directory_watch_options& options, + file_event_callback callback, std::string* error_message) { + std::error_code ec; + if (!std::filesystem::is_directory(options.path, ec)) { + if (error_message) *error_message = ec ? ec.message() : "Watch path is not a directory"; + return {}; + } + if (!callback) { + if (error_message) *error_message = "A file event callback is required"; + return {}; + } + if (options.poll_interval <= std::chrono::milliseconds::zero()) { + if (error_message) *error_message = "Poll interval must be positive"; + return {}; + } + return directory_watcher(std::make_unique(options, + std::move(callback))); +} + +} // namespace izo diff --git a/src/time.cpp b/src/time.cpp new file mode 100644 index 0000000..906ad89 --- /dev/null +++ b/src/time.cpp @@ -0,0 +1,13 @@ +#include + +#include + +namespace izo { + +monotonic_clock::time_point monotonic_now() noexcept { return monotonic_clock::now(); } + +void sleep_for(std::chrono::nanoseconds duration) { + if (duration > duration.zero()) std::this_thread::sleep_for(duration); +} + +} // namespace izo